turnstile · script fails to load

Failed to load the Turnstile script

Either api.js never executes, or it executes too late and turnstile.render() throws turnstile is not defined. Both present as "the widget just is not there". The four causes worth checking are a Cloudflare optimisation rewriting your script tag, a Content Security Policy missing a directive, an extension or network blocking challenges.cloudflare.com, and an ordering mistake with explicit rendering. The first is the one nobody expects, because it is Cloudflare breaking Cloudflare.

Rocket Loader, the cause nobody looks for

Rocket Loader is a Cloudflare speed feature, on by default in some configurations, and it does exactly what it says: it defers the loading of all of your JavaScript until after rendering, inline and external alike. Cloudflare's own description is that it "handles both inline and external scripts, while maintaining order of execution".

That is fine for most scripts and wrong for this one. Turnstile's implicit rendering mode depends on api.js running and finding cf-turnstile containers in the document; explicit rendering depends on api.js having defined window.turnstile before your onload callback fires. Rewriting when scripts run rewrites both of those assumptions, and the failure is intermittent enough to look like a network problem.

Cloudflare's own limitations note is blunt about the general case: "If you observe JavaScript or jQuery issues for your website, disable Rocket Loader and retest your website." That is the diagnostic. Turn it off, reload, and if the widget appears you have your answer in thirty seconds.

The fix is not to leave it off. Exclude the one script instead:

html
<!-- data-cfasync="false" tells Rocket Loader to leave this tag alone. -->
<script data-cfasync="false" defer
  src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>

<div class="cf-turnstile" data-sitekey="0x4AAAAAAA_your_key"></div>

If a whole page is affected rather than one tag, Cloudflare's documented alternative is a Configuration Rule excluding that URL. Note that data-cfasync="false" has to be on the tag itself — adding it to a wrapper, or setting it from script after the fact, does nothing.

Rocket Loader also has a CSP consequence that is easy to miss. It loads its own runtime from ajax.cloudflare.com, so a policy that does not allow that origin blocks Rocket Loader itself — which can leave every deferred script on the page, Turnstile included, never running at all.

A Content Security Policy missing a directive

Cloudflare states it directly: "Without the correct CSP directives, Turnstile may fail to load." Two directives are the minimum:

DirectiveValueWhat breaks without it
script-srchttps://challenges.cloudflare.comapi.js is blocked outright — nothing renders
frame-srchttps://challenges.cloudflare.comThe script runs, the challenge iframe is refused, the widget hangs
connect-src'self'Pre-clearance only — the cf_clearance fetch fails silently

The two failure shapes are worth telling apart. A missing script-src entry means api.js never runs, so window.turnstile is undefined and nothing appears. A missing frame-src entry means the script runs fine and the iframe is refused — the container appears, then does nothing, which reads as a hung widget rather than a load failure and sends people down the wrong path entirely.

Cloudflare recommends the nonce-based approach over an allowlist: put your nonce on the api.js tag and Turnstile propagates it to the resources it loads. It also works with strict-dynamic. One caveat from their reference worth knowing before you try to fix this with infrastructure: you cannot set your own CSP through meta tags or Transform Rules on challenge pages.

Every CSP failure names itself in the console — Refused to load the script or Refused to frame, with the directive that blocked it. Read that line before changing anything; it identifies the exact directive.

Blocked by an extension, a network, or a filter

Cloudflare's troubleshooting sequence starts here for a reason. Their first three steps are browser compatibility, then extensions — "Some browser extensions, such as ad blockers, may block the scripts Turnstile needs to operate" — then JavaScript being enabled at all. Private mode, a different browser, no VPN and a different network follow.

When the iframe specifically cannot load, Turnstile has a code for it: 200500, documented as "Iframe load error", with the instruction to check whether challenges.cloudflare.com is blocked. It is marked retryable. If you see 200500 rather than nothing at all, the script loaded and the challenge frame did not — which points at frame-src or a network filter, not at the script tag.

One thing that looks like this and is not: DNS lookup failures on subdomains of challenges.cloudflare.com. Cloudflare documents those as part of normal execution and explicitly non-blocking, and advises against surfacing them as fatal — particularly in WebView and handler-based integrations, where the recommended approach is to drop network errors from the *.challenges.cloudflare.com wildcard while keeping visibility on the apex. A HAR full of red that ends in a working token is not a load failure.

Explicit rendering, called too early

With ?render=explicit, api.js no longer scans the document — you call turnstile.render() yourself. That makes the ordering yours to get right, and turnstile is not defined is what getting it wrong looks like.

html
<!-- Wrong: turnstile may not exist yet when this runs. -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
<script>turnstile.render("#box", { sitekey: "0x4AAAAAAA_your_key" });</script>

<!-- Right: name a callback and let api.js invoke it when it is ready. -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onTurnstileLoad"
        defer></script>
<script>
  function onTurnstileLoad() {
    turnstile.render("#box", {
      sitekey: "0x4AAAAAAA_your_key",
      callback: function (token) { console.log("token", token.length, "chars"); },
      "error-callback": function (code) { console.log("turnstile error", code); }
    });
  }
</script>

The callback has to be reachable as a global by the name in the query string. Defined inside a module, a bundler scope or a framework component, it is not — which is the usual reason this pattern fails in a build that worked as a plain HTML file. In a single-page app, render on mount and keep the container out of the framework's control, since the iframe and the hidden input are inserted into it from outside.

If you are trying to establish whether a page carries Turnstile at all, and with what configuration, our Turnstile checker reads the served HTML and reports the sitekey, the rendering mode and the widget's attributes.

A five-minute order of checks

  • Console first. Refused to load names the CSP directive. turnstile is not defined is an ordering problem. Silence with no widget points at the script tag never running.
  • Network panel. Is api.js requested at all? Not requested means the tag was rewritten or removed — Rocket Loader, a tag manager, a sanitiser. Requested and failed means CSP or a blocker.
  • View source, not the DOM inspector. The inspector shows the page after scripts have run. Source shows what the server sent, which is where a rewritten type="text/rocketscript" attribute is visible.
  • Turn Rocket Loader off and reload. Thirty seconds, and it settles the most surprising cause outright.
  • Try a private window with extensions disabled. Cloudflare's own step 2 and 4, and it separates "broken for everyone" from "broken for me".

If the script loads and the widget still fails, the problem has moved on from loading. A numeric code in error-callback is the fastest route from there: the error index maps each one to its cause, and a widget that mounts and then stops responding is error 300030 rather than a load failure at all.

Common questions

Rocket Loader defers every script on the page so content renders sooner, and it cannot know that this one has ordering requirements. Turnstile's implicit mode needs api.js to run and find its containers; explicit mode needs window.turnstile to exist before your onload callback. Excluding the tag with data-cfasync="false" keeps both features.

script-src and frame-src must both allow https://challenges.cloudflare.com. Pre-clearance additionally needs 'self' in connect-src, because the clearance cookie is set by a fetch to a /cdn-cgi/ endpoint on your own domain. Cloudflare recommends a nonce on the api.js tag instead of an allowlist, and Turnstile works with strict-dynamic.

No — that is usually frame-src rather than script-src. The script running and the challenge iframe being refused produces an empty container, not a missing one, and Turnstile reports it as error 200500, documented as an iframe load error with the advice to check whether challenges.cloudflare.com is blocked.

Almost certainly not. Cloudflare documents those DNS lookup failures as part of Turnstile's normal execution and explicitly non-blocking, and advises against treating them as fatal — especially in WebView embeddings. If a token is being issued, they are noise.

Because it ran before api.js finished. With render=explicit, pass onload=yourCallback in the script URL and do the rendering inside that function — and make sure the callback is a real global, not a name scoped inside a module or bundler output, which is why this often breaks only after a build step.

Related

Sources

More in Turnstile errors

Turnstile error 300030Error 300030 means a Turnstile widget mounted and then stopped responding. The real causes, the codes it gets confused with, and the fixes.Turnstile timeout-or-duplicate errorsiteverify returns timeout-or-duplicate when a Turnstile token is verified twice or is older than 300 seconds. The causes, and how to fix each one.Turnstile callback not firingWhy the Turnstile success callback never runs: implicit rendering, callbacks not on window, hidden containers and SPA remounts.Cloudflare Turnstile error 600010Turnstile error 600010 is a generic challenge failure in the 600* family. What Cloudflare documents, the reproduced causes, and how to fix it.cf-turnstile-response invalidThree different failures wear this name. Sort out which one you have — the token never arrived, siteverify refused it, or your keys do not match.Turnstile stuck on "Verifying you are human"Why Turnstile hangs on "Verifying you are human": how to read the error code, plus fixes for CSP, clock skew, blocked domains and headless CI.invalid-input-responsesiteverify returns invalid-input-response when a token is invalid, malformed or expired. The causes worth checking, in the order they occur.invalid-input-secretsiteverify returns invalid-input-secret when your secret key is rejected. Why the key is usually right, and the five things that actually cause it.Token rejected"Token rejected" is not a Cloudflare error — it is what an app prints when siteverify refused its token. What to do, as a visitor or as a developer.Field is requiredA Laravel validation message meaning the Turnstile token never reached your server. Why the field is empty, how to tell which cause it is, and the fix for each.Turnstile 401 in consoleA 401 on a /cdn-cgi/challenge-platform/ pat/ request is expected. Cloudflare documents it: the browser could not get a Private Access Token, and falls back.Turnstile verification loopThe widget completes, then challenges again. Cloudflare calls it a challenge loop. The five documented causes, how to tell them apart, and what each one fixes.

Automating a gate you own or are authorised to test?

// SolveGate clears Cloudflare Turnstile and WAF challenges through one REST call · first 1,000 solves free