How long is a Turnstile token valid?
A Cloudflare Turnstile token is valid for 300 seconds (5 minutes) from the moment it is generated, and it can be validated exactly once. After the window closes, or after one successful call to the siteverify API, the token is dead and the widget has to produce a new one.
The 300-second window
Cloudflare's server-side validation documentation is explicit on both points: "Each token is valid for 300 seconds (5 minutes) after generation" and "Each token can only be validated once. A replayed token will be rejected with the timeout-or-duplicate error code."
| Property | Documented value |
|---|---|
| Token validity | 300 seconds (5 minutes) from generation |
| Validations per token | One. Replay is rejected |
| Maximum token length | 2048 characters |
| Rejection code when expired or reused | timeout-or-duplicate |
| Default expiry behaviour in the widget | refresh-expired: auto |
The clock starts at generation, not at form submission. With the default execution mode (execution: render), the challenge runs as soon as the widget renders, so a token minted on page load is already partway through its life by the time a visitor finishes typing. Cloudflare puts it plainly: "the visitor must initiate the request and submit the token to your backend within the five-minute window. Otherwise, the Turnstile widget needs to be refreshed to generate a new token."
If your form regularly takes longer than five minutes to complete, defer the challenge instead of widening the window — the window is not configurable. Set execution: "execute" and call turnstile.execute() at submit time so the token is fresh when it reaches your server.
Single use: siteverify redeems the token
A Turnstile token is a bearer credential that you spend. You spend it by POSTing it to https://challenges.cloudflare.com/turnstile/v0/siteverify with your secret key. The endpoint accepts application/x-www-form-urlencoded or application/json and always responds with JSON.
const SECRET_KEY = process.env.TURNSTILE_SECRET_KEY; async function validateTurnstile(token, remoteip) { const res = await fetch( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret: SECRET_KEY, response: token, remoteip, // Optional: a UUID you generate so a retried request is not // treated as a second redemption of the same token. idempotency_key: crypto.randomUUID(), }), }, ); const result = await res.json(); // result.success, result.challenge_ts, result.hostname, // result["error-codes"], result.action, result.cdata return result; }
The call is the redemption. A second call with the same response value fails, regardless of how much of the five minutes is left. That is why expiry and reuse collapse into one error code — from the server's point of view, both mean "this token is no longer spendable".
| Error code | Cloudflare's description | Action required |
|---|---|---|
timeout-or-duplicate | Token has already been validated | Each token can only be used once |
invalid-input-response | Token is invalid, malformed, or expired | User should retry the challenge |
missing-input-response | Response parameter was not provided | Ensure token is included |
invalid-input-secret | Secret key is invalid or expired | Check your secret key in the Cloudflare dashboard |
bad-request | Request is malformed | Check request format and parameters |
internal-error | Internal error occurred | Retry the request |
Network retries are the classic way to burn a token by accident: your first siteverify request succeeds at Cloudflare but the response is lost, you retry, and the retry comes back timeout-or-duplicate. The optional idempotency_key parameter — "a UUID you generate to safely retry validation requests" — exists for exactly this. Generate it once per token and reuse it across retries of that token.
Expiry and interactive timeout are different events
Turnstile fires two distinct callbacks, and conflating them produces confusing bug reports. expired-callback is "invoked when the token expires and does not reset the widget". timeout-callback is "invoked when the challenge presents an interactive challenge but was not solved within a given time" — and unlike the expiry callback, "a callback will reset the widget to allow a visitor to solve the challenge again".
- **Expiry** means a token existed, was handed to your
callback, and has now aged past 300 seconds. Cloudflare does not document a duration for the interactive timeout. - **Timeout** means no token was ever produced, because an interactive challenge sat unsolved. This only arises on Managed widgets that escalate to interaction.
- The widget's own error codes distinguish them too:
110600is "Challenge timed out" and110620is "Interaction timed out", both marked retryable, with110620explicitly resolved by "Reset with turnstile.reset()".
If you are logging failures, keep the two paths separate. A spike in expired-callback points at slow forms or a page that renders the widget too early. A spike in timeout-callback points at a widget visitors are not noticing.
Auto-refresh is the default, and turnstile.reset() is the manual lever
You usually do not have to write refresh logic, because Turnstile ships with it enabled. refresh-expired "automatically refreshes the token when it expires. Can take auto, manual, or never, defaults to auto." With auto, the widget re-runs the challenge on expiry and delivers the replacement token through your callback, so a long-lived page keeps a live token in the cf-turnstile-response field.
| Parameter | Values | Default | Applies to |
|---|---|---|---|
refresh-expired | auto, manual, never | auto | Token expiry |
refresh-timeout | auto, manual, never | auto | Interactive timeout, Managed widgets only |
retry | auto, never | auto | Failure to obtain a token |
retry-interval | Positive integer under 900000 (ms) | 8000 | Spacing between retries when retry: auto |
refresh-timeout "controls whether the widget should automatically refresh upon entering an interactive challenge and observing a timeout", with manual prompting the visitor to refresh and never showing a timeout. Cloudflare notes it "only applies to widgets of Managed mode".
Set either to never and the responsibility moves to you. In explicit rendering mode, the widget JavaScript API gives you what you need to manage the lifecycle yourself.
const widgetId = turnstile.render("#turnstile-container", { sitekey: "<YOUR-SITE-KEY>", "refresh-expired": "never", callback: (token) => { document.querySelector("#submit").disabled = false; }, "expired-callback": () => { document.querySelector("#submit").disabled = true; }, }); // Right before submitting, make sure the token is still spendable. document.querySelector("#form").addEventListener("submit", (event) => { if (turnstile.isExpired(widgetId)) { event.preventDefault(); turnstile.reset(widgetId); // discard state and run a fresh challenge return; } // turnstile.getResponse(widgetId) holds the current token });
turnstile.getResponse(widgetId)— "retrieve the current response token at any time".turnstile.isExpired(widgetId)— check whether the widget's token has aged out.turnstile.reset(widgetId)— "reset the widget if the given widget timed out or expired".turnstile.remove(widgetId)— "will not call any callback and will remove all related DOM elements".
The cf_clearance cookie has its own, longer lifetime
Do not confuse the token's 300 seconds with the lifetime of a WAF clearance. They are separate mechanisms with separate clocks. By default a Turnstile widget issues only a one-time token. When you enable pre-clearance on the widget, "a cf_clearance cookie is issued to the visitor in addition to the default Turnstile token", and that cookie bypasses WAF challenges on the zone at or below the configured clearance level (interactive, managed, jschallenge, or no_clearance, which is the default).
Cloudflare states that "clearance cookies generated by the Turnstile widget will be valid for the time specified by the zone-level Challenge Passage value". The cf_clearance cookie "has a lifetime of 30 minutes" by default, with Cloudflare recommending "a setting between 15 and 45 minutes". A few extra minutes are allowed on evaluation to absorb clock skew, and "for XmlHTTP requests, an extra hour is added to the validation time".
Clearance is not purely time-bound. Cloudflare's Precursor clearance is "continuously re-evaluated throughout a visitor's session", and if a session looks suspicious the effective clearance "may be reduced or invalidated" and the visitor "may be re-challenged, even if the cookie has not expired". Treat a clearance cookie as a lease that can be revoked, not a fixed 30-minute pass.
Designing automation around a five-minute token
For QA suites, CI jobs and uptime checks that drive a Turnstile-protected flow, the lifetime is the binding constraint. Everything between obtaining the token and your application redeeming it has to fit inside 300 seconds, and each token covers exactly one submission.
- Obtain the token as late as possible in the run, not during setup. A token fetched at the start of a ten-minute suite is worthless by step three.
- Never cache or share tokens between test cases. The second use returns
timeout-or-duplicate, which reads like a flaky test but is correct behaviour. - Budget for latency you do not control. A token acquired with two minutes of life left leaves little room for a slow backend.
- Assert on
error-codes, not justsuccess.timeout-or-duplicateandinvalid-input-responsepoint at different bugs in your harness.
This is where a solver fits into a test harness. SolveGate returns a Turnstile token for a sitekey and URL you control, typically in under 1.5 seconds, which leaves the overwhelming majority of the five-minute window for your own request path.
// Acquire the token immediately before the request that spends it. const res = await fetch(`${process.env.SOLVEGATE_BASE_URL}/v1/solve`, { method: "POST", headers: { authorization: `Bearer ${process.env.SOLVEGATE_SECRET_KEY}`, "content-type": "application/json", }, body: JSON.stringify({ gate: "turnstile", // or "waf" for a Turnstile WAF challenge page sitekey: process.env.STAGING_SITEKEY, url: "https://staging.example.com/login", }), }); const { token } = await res.json(); // Spend it once, now. Do not stash it for the next test case. await fetch("https://staging.example.com/login", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ email: "qa@example.com", password: process.env.STAGING_PASSWORD, "cf-turnstile-response": token, }), });
Run this against properties you own or are authorised to test — your staging environments, your CI pipelines, your uptime monitoring, your own anti-bot configuration. That is the boundary.
SolveGate handles Cloudflare Turnstile (managed, non-interactive and invisible) and Turnstile WAF challenge pages. GET /v1/solve/{id} polls a job and is free. Failed solves are never billed. Prepaid credits run from $0.40 per 1,000 solves down to $0.075 at volume, and the first 1,000 solves are free. SDKs are published as solvegate on npm (Node 18+) and PyPI (Python 3.9+).
Common questions
300 seconds — five minutes — from the moment it is generated. Cloudflare does not expose a setting to extend this. If your form takes longer to fill in, render the widget with execution: "execute" and call turnstile.execute() at submit time so the token is generated late.
No. Each token can be validated exactly once. A second siteverify call with the same token returns success: false with timeout-or-duplicate in error-codes, even if the five-minute window has not closed. If you need to retry a validation request safely after a network failure, pass the same idempotency_key UUID on the retry.
It means the token is no longer spendable: either it aged past 300 seconds or it has already been redeemed. Cloudflare's error table describes it as "Token has already been validated". The fix is always the same — get a fresh token from the widget, via turnstile.reset() or the default auto-refresh.
Yes, by default. refresh-expired defaults to auto, which re-runs the challenge when the token expires and delivers the new token through your callback. Set it to manual to prompt the visitor, or never to take full control, in which case you handle refreshing yourself with turnstile.reset().
expired-callback fires when a token you already received has aged out; it does not reset the widget. timeout-callback fires when an interactive challenge was presented but not solved in time, and it does reset the widget so the visitor can try again. The related widget error codes are 110600 (challenge timed out) and 110620 (interaction timed out).
Longer, and on a separate clock. When pre-clearance is enabled on a Turnstile widget, the cf_clearance cookie lasts for the zone's Challenge Passage value, which defaults to 30 minutes; Cloudflare recommends 15 to 45 minutes. Clearance can also be reduced or invalidated early if Cloudflare's Precursor signals judge the session suspicious.
Related
More in Glossary
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