DEV Community

Peak Fo
Peak Fo

Posted on Originally published at blog.peak.fo

How to Fix Cloudflare Turnstile Invisible Mode (Silent Failures)

Cross-posted from blog.peak.fo. I work on Peak, a Cloudflare Turnstile solving API, so the code below calls it. The sitekey-reading and token-injection steps apply no matter which solver you use.


You dropped a Turnstile widget on your form, everything worked in the browser, and then your automation or test harness hit a wall. The widget renders nothing. No checkbox, no spinner, no "verify you are human" box to click. The form won't submit, and your logs show an empty cf-turnstile-response field. Welcome to Turnstile invisible mode, where the failure is silent by design.

This post walks through why invisible and managed Turnstile stall, how to tell which mode you're dealing with, and the fixes in the order you should try them. Framing up front: this is for QA, automated testing, and scraping public data where you respect the site's terms and robots rules. Not credential stuffing, not account abuse.

The three Turnstile modes, and why that matters

Cloudflare Turnstile runs in one of three modes, set by whoever configured the widget in the Cloudflare dashboard. You don't pick the mode from your side; you inherit it from the sitekey.

Mode What the user sees When the token is issued
Managed Usually nothing, sometimes an interactive checkbox if Cloudflare decides it needs one After passive checks pass, or after the user clicks the checkbox Cloudflare shows
Non-interactive A visible widget box that never asks for interaction After passive browser and network checks pass
Invisible Nothing at all, zero rendered pixels Silently in the background once checks pass

Managed is the default and the one most people ship. It's adaptive: Cloudflare keeps it invisible when it trusts the visitor and escalates to a clickable challenge when it doesn't. So managed can behave like invisible right up until it decides to show you something. For the full breakdown of how the challenge runs under the hood, see what Cloudflare Turnstile is and how it works.

The practical takeaway: invisible and managed-while-invisible both produce the same symptom. No visible element, and a token that either appears on its own or never does.

Why invisible mode silently fails

The Turnstile script runs a battery of passive checks: browser fingerprint, JavaScript execution environment, TLS and header signals, and the reputation of the IP you're coming from. If every check passes, it writes a token into a hidden <input name="cf-turnstile-response"> and fires the widget's success callback. Your form's submit handler is usually waiting on that callback.

A failed check on invisible mode has no fallback UI to drop to. On managed mode you'd get an interactive checkbox. On invisible mode there's nothing to render, so the script waits. The token never lands, the callback never fires, and your submit button stays disabled, or your POST goes out with an empty token field and gets rejected server-side.

The two most common reasons the passive checks fail:

  • Headless or automated browser signals. A bare headless Chrome leaks a dozen tells. Turnstile reads them and declines to issue a token.
  • Flagged IP reputation. Datacenter IPs, known proxy ranges, and addresses with abuse history score badly. This is the one people miss, because the same code works from a home connection and fails from a cloud VM.

That second point is the big one. Turnstile passes far more readily from a residential or ISP IP than from a datacenter range. If your token is empty specifically when you run from a server, the IP is the real problem, and no amount of client-side fiddling fixes it. More on that below.

Figuring out which mode you're facing

You can't ask Cloudflare, but you can read the page. Everything you need is in the markup and the render parameters.

Start with the sitekey. It lives on the element Turnstile mounts into, as data-sitekey, and it always starts with 0x:

<div class="cf-turnstile"
     data-sitekey="0x4AAAAAAAxxxxxxxxxxxxxx"
     data-callback="onTurnstileSuccess"></div>
Enter fullscreen mode Exit fullscreen mode

If the widget is injected by JavaScript with the explicit render API instead, the sitekey is passed in the call:

turnstile.render('#container', {
  sitekey: '0x4AAAAAAAxxxxxxxxxxxxxx',
  callback: function(token) { /* ... */ }
});
Enter fullscreen mode Exit fullscreen mode

The data-callback (or callback) attribute is what you'll need to fire later, so note its name now. If there's no visible box anywhere on the page but a cf-turnstile element exists in the DOM, you're on invisible or managed. If a box renders but never asks for a click, that's non-interactive. If a checkbox appears, that's managed escalating. For the deep version of locating the key across inline attributes, JS config, and network calls, see how to find a Cloudflare Turnstile sitekey.

Fix 1: read the sitekey correctly even when nothing renders

Invisible mode doesn't hide the sitekey, it hides the UI. The data-sitekey is still in the DOM. Grab it from the attribute rather than looking for a visible widget.

In the browser or a Playwright/Puppeteer context:

// Pull the sitekey no matter how the widget was configured
const sitekey =
  document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')
  || (window.turnstile && document.querySelector('.cf-turnstile')
        ?.getAttribute('data-sitekey'));

console.log('sitekey:', sitekey);
Enter fullscreen mode Exit fullscreen mode

If the widget is rendered via the explicit JS API and the sitekey never touches the DOM, read it from the network: the request to challenges.cloudflare.com carries it. For the token-field mechanics first, the cf-turnstile-response token explained covers where it lives and what consumes it.

Fix 2: get a token from a solving API and inject it

Once you have the sitekey and the page URL, you can request a valid token from a solver and place it into the form yourself. This is the fix that clears invisible mode, because the solver runs the challenge in an environment that passes the checks and hands you back a real token.

Peak solves Turnstile (and the 5-second challenge) and returns a token in about a second. You pay only for solves that succeed. The request in Python:

import requests

resp = requests.post(
    "https://api.peak.fo/solve",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "task_type": "turnstiletaskproxyless",
        "url": "https://example.com/login",
        "sitekey": "0x4AAAAAAAxxxxxxxxxxxxxx",
    },
)

data = resp.json()
# {"success": true, "data": {"token": "1.7Nm..."}, "cost": 0.0009}
token = data["data"]["token"]
print(token)
Enter fullscreen mode Exit fullscreen mode

Now inject it. The critical part people get wrong: setting the field value isn't enough. The page's framework may not notice a raw value assignment, and the submit handler is usually waiting on the callback, not polling the field. So you set every cf-turnstile-response field, dispatch input and change so reactive frameworks pick it up, then call the widget's data-callback.

function injectTurnstileToken(token, callbackName) {
  // 1. Fill every token field (some pages render more than one)
  document
    .querySelectorAll('[name="cf-turnstile-response"], #cf-chl-widget-response')
    .forEach((el) => {
      el.value = token;
      el.dispatchEvent(new Event('input',  { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
    });

  // 2. Fire the success callback the form is waiting on
  if (callbackName && typeof window[callbackName] === 'function') {
    window[callbackName](token);
  }
}

// callbackName is the data-callback value you read in Fix 1
injectTurnstileToken('1.7Nm...', 'onTurnstileSuccess');
Enter fullscreen mode Exit fullscreen mode

If you can't find a named callback, the form may gate submission purely on the field value, in which case step 1 plus a manual form submit does it. But check for the callback first; on invisible mode it's almost always there, and it's why the button stays dead otherwise. For the full browser-driver pattern, including waiting for the widget to mount before you read the sitekey, see handling Turnstile in Playwright and Puppeteer, and there's a copy-paste Cloudflare Turnstile Python example on the site.

Fix 3: pass your own proxy when the IP is the real problem

If you've got the sitekey right and the injection right and it still won't clear, the token you're minting is being validated against the wrong IP. Turnstile tokens are tied to the context they were issued in. A proxyless solve mints the token from the solver's egress IP. If the target site correlates the token with your request IP, or if your own IP is the thing getting flagged on submit, you need the token minted from an IP you control.

Switch from the proxyless task to the task that takes a proxy, and pass your own egress:

{
  "task_type": "turnstiletask",
  "url": "https://example.com/login",
  "sitekey": "0x4AAAAAAAxxxxxxxxxxxxxx",
  "proxy": "http://user:pass@your-proxy-host:port"
}
Enter fullscreen mode Exit fullscreen mode

Use a residential or ISP proxy here, not another datacenter IP, or you've moved the reputation problem sideways. The point is to mint the token from the same kind of address your actual request comes from, so the two line up.

What won't fix it

A few things people try that waste an afternoon:

  • Hardcoding or reusing an old token. Tokens are single-use and short-lived. One that worked five minutes ago is dead.
  • Spoofing the user agent alone. Turnstile reads far more than the UA string. Changing it does nothing on its own.
  • Setting the field value without firing the callback. On invisible and managed mode this is the number one reason an otherwise correct token does nothing. The submit handler waits on the callback.
  • Grinding on client-side code when the IP is flagged. If it works locally and fails on a server, stop editing JavaScript and fix the IP (Fix 3).

Rule of thumb: if nothing renders and nothing submits, confirm the sitekey, then confirm you're firing the callback, then confirm the IP. In that order. Most stalls are solved by the first two; the stubborn ones are always the third.

Putting it together

Invisible Turnstile isn't broken when it shows you nothing. It's waiting on a token that your environment can't earn. Read the sitekey straight from data-sitekey, request a token scoped to the right URL and IP, write it into every cf-turnstile-response field, dispatch the events, and fire the callback. When a server-side run still stalls after all that, the IP reputation is the lever, so mint the token through your own proxy.

FAQ

Why does Turnstile show nothing at all instead of a checkbox?
That's invisible mode by design. The widget runs its passive checks silently and either writes a token on success or sits there on failure. There's no fallback UI to show you, unlike managed mode, which can escalate to a visible checkbox.

Can I hardcode a token I captured earlier?
No. Turnstile tokens are single-use and expire within minutes. A token that worked five minutes ago gets rejected by siteverify now.

I set the token field and the form still won't submit. Why?
Most invisible and managed widgets gate submission on the data-callback firing, not on the field value changing. Read the callback name off the widget's render config and call it directly with the token.

Does a proxy fix every invisible-mode stall?
No. A proxy only matters when the IP your token was minted from needs to match the IP your request comes from. If the sitekey and callback are both wrong, a proxy changes nothing. Fix those first.

What does this cost?
Turnstile solves run $0.90 per 1,000 successful solves, dropping to $0.35 per 1,000 at volume. Failed solves are free, and new accounts get 1,000 free solves with no card.

For the token's full lifecycle, see the cf-turnstile-response token explained. For the IP side of the decision, see how to solve Cloudflare Turnstile without a proxy.

Start free at peak.fo. 1,000 free solves, no card, pay only for solves that land.

Top comments (0)