cf-turnstile-response Is Empty: Why the Turnstile Token Isn't Populating

An empty cf-turnstile-response almost always means the challenge never completed before you read it. The causes, in order, and how to fix each.

The form won't submit. You check the hidden input, cf-turnstile-response, and it's empty. No token, no error in the console, just a blank value where the proof of a solved challenge should be. The widget looks fine on screen. So where's the token?

An empty cf-turnstile-response almost always means one thing: the challenge never actually completed before you read the value. The fixes all come down to when and where you're reading it. Here are the causes, roughly in order of how often they're the real one.

1. You read the token before the challenge finished

This is the big one. Turnstile is asynchronous. The widget renders, then runs its challenge, then populates cf-turnstile-response and fires its callback. If your code reads the field on page load, on a timer, or the instant the user clicks submit, the token may simply not be there yet.

The right pattern is to let Turnstile tell you when it's done. Use the callback option:

turnstile.render('#widget', {
  sitekey: '0x4AAAAA...',
  callback: function (token) {
    // token is guaranteed to exist here
    document.querySelector('#my-form button').disabled = false;
  },
});

Keep the submit button disabled until the callback fires. That alone fixes a large share of empty-token reports, because it makes it impossible to submit before the token exists.

2. The widget rendered, but the challenge silently failed

If the challenge itself errored out, there's no token to populate, and depending on your setup you might not see a loud failure. A domain mismatch (110200), a blocked challenges.cloudflare.com request, or an aggressive privacy extension can all leave you with a rendered-looking widget and an empty field.

Wire up the error-callback so failures stop being silent:

turnstile.render('#widget', {
  sitekey: '0x4AAAAA...',
  callback: onToken,
  'error-callback': function (code) { console.warn('Turnstile error', code); },
  'expired-callback': function () { console.warn('Turnstile token expired'); },
});

If error-callback fires with a code, that's your real problem, and our guide to Turnstile error codes covers what each one means.

3. The token expired while the user sat on the page

A Turnstile token is good for 300 seconds. If someone solves the challenge, then fills out a long form, gets distracted, and submits ten minutes later, the token in the field is stale. Some integrations clear it on expiry, some don't, and a stale token verifies as timeout-or-duplicate on your backend even though the field looked populated.

Handle expired-callback by resetting the widget (turnstile.reset()) and re-disabling submit, so the user gets a fresh challenge instead of a dead token.

4. Two widgets, or a widget rendered twice

If you call render() twice on the same container, or you have the auto-render (the cf-turnstile class) and an explicit render() call, you can end up with a confused widget that never populates the field you're reading. Single-page apps are especially prone to this, because navigating back to a form re-runs your mount code.

Render once. In an SPA, explicitly remove() or reset() the widget on unmount, and give each instance a unique container.

5. You're automating, and the challenge won't complete at all

Everything above assumes a real person in a real browser. If you're driving Turnstile with Selenium, Puppeteer, Playwright, or an HTTP client, an empty cf-turnstile-response usually means the challenge detected automation and refused to issue a token. There's nothing to read because nothing was granted.

This is a different problem from the timing bugs above, and it doesn't have a tidy front-end fix. The challenge fingerprints the runtime, the TLS handshake, and the IP, and a headless or stealth-patched browser on a datacenter IP tends to come up short. You can chase it, patching automation flags and sourcing cleaner proxies, and we cover the moving pieces in handling Turnstile in Playwright and Puppeteer. But it's an arms race, and Cloudflare updates its side regularly.

The alternative is to not render the widget in your automation at all. A solving API runs the challenge for you and hands back a valid cf-turnstile-response token that you drop into the form exactly as if a human had solved it. You send the sitekey and the page URL; a token comes back in about a second. We run roughly 8,000 of these a minute at around 97% success, and because you pay only for tokens that actually work, an empty response costs you nothing.

A short checklist

  • Is the submit button gated behind the callback? If not, start there.
  • Is error-callback firing? If yes, read the code.
  • Has it been more than five minutes since the solve? Reset on expired-callback.
  • Are you rendering the widget exactly once? Check for double-mounts.
  • Are you automating? Then the empty field is detection, not a bug, and you need a different approach.

Nine times out of ten in a normal browser, it's the first item. Gate the submit on the callback and the empty token goes away. If you're automating, the empty token is the challenge doing its job, and that's the case Peak is built for.