Skip to content

A modern signup form stacks two independent gates: a bot check before the request is accepted, and an email verification after. They fail in different ways, and confusing the two produces integration bugs that look like the challenge is broken.

How Turnstile's token actually behaves

Cloudflare's Turnstile issues a token to the client, the client submits it with the form, and the server validates it against the Siteverify API. The published constraints are exact and each one has caused a production bug somewhere:

  • Endpoint: POST https://challenges.cloudflare.com/turnstile/v0/siteverify, accepting either form-encoded or JSON, always returning JSON.
  • Required parameters: secret (the widget secret) and response (the token).
  • Optional parameters: remoteip for the visitor's address, and idempotency_key as a UUID to make retries safe.
  • Token lifetime: 300 seconds, five minutes, from generation.
  • Single use: each token validates exactly once.
  • Maximum length: 2048 characters.

The two that bite are lifetime and single use. A user who fills a long form slowly, or who submits, gets an error, and retries, is now sending a token that either expired or was already spent. The Siteverify response says so with timeout-or-duplicate, which is a specific condition and not a generic failure.

The single-use trap in retry logic

Here is the sequence that produces a confusing bug. The client submits. The server validates the token successfully. Then a downstream step fails — a database write, a mail send, anything. The server returns a 500. The user hits submit again with the same rendered form, sending the token that was already consumed. Siteverify returns timeout-or-duplicate, the UI shows "verification failed," and the actual failure was three layers away.

The fix is to make the client obtain a fresh token whenever the previous submission did not succeed end to end. Turnstile's own widget flow re-issues on reset, so the integration needs to call reset on any non-success path. The idempotency key exists for the adjacent problem: retrying the same validation is safe, retrying with a spent token is not.

Where the token must be validated

Only on the server, only with the secret, and always before the side effect. A token validated after the account row is created is decoration. Validating in the browser is equivalent to not validating at all, since the browser is the untrusted party. And the secret must never reach the client — the same discipline that applies to a mail provider's API key.

The optional remoteip parameter is worth passing when the request comes through a trusted proxy, because it binds the token to the address that solved the challenge. Behind a reverse proxy that means resolving the real client address from a header you control, which is the same problem as any X-Forwarded-For handling — trust it only when the immediate peer is your own proxy.

What bot protection does not do

Turnstile answers one question: did a human-shaped client solve this challenge. It says nothing about whether the email address behind the form is real, reachable, or disposable. A perfect Turnstile pass with a fabricated address produces a perfectly valid request that can never be verified.

That is why the two gates are complementary rather than redundant. Bot protection raises the cost of automated submission. Email verification confirms that the claimed channel actually exists. Neither is sufficient, and stacking them is what makes a funnel both cheap to operate and hard to farm.

Instrumentation worth having

Log the failure code, not just the boolean. timeout-or-duplicate means the client needs a reset. A secret mismatch is a deployment problem. A network failure to Siteverify is an availability problem on your side, not the user's. Collapsing all three into "captcha failed" makes every incident a guess, and the fix for each is different.

The takeaway

Treat the Turnstile token as a five-minute, single-use credential with a 2048 character ceiling, reset it on every non-success path, validate it server-side before any side effect, and remember that it proves a client solved a challenge — nothing more. Email verification is the gate that proves the channel exists, and the two belong in the same funnel for different reasons.

Keep reading