glossary · turnstile token lifetime

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."

PropertyDocumented value
Token validity300 seconds (5 minutes) from generation
Validations per tokenOne. Replay is rejected
Maximum token length2048 characters
Rejection code when expired or reusedtimeout-or-duplicate
Default expiry behaviour in the widgetrefresh-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.

js
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 codeCloudflare's descriptionAction required
timeout-or-duplicateToken has already been validatedEach token can only be used once
invalid-input-responseToken is invalid, malformed, or expiredUser should retry the challenge
missing-input-responseResponse parameter was not providedEnsure token is included
invalid-input-secretSecret key is invalid or expiredCheck your secret key in the Cloudflare dashboard
bad-requestRequest is malformedCheck request format and parameters
internal-errorInternal error occurredRetry 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: 110600 is "Challenge timed out" and 110620 is "Interaction timed out", both marked retryable, with 110620 explicitly 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.

ParameterValuesDefaultApplies to
refresh-expiredauto, manual, neverautoToken expiry
refresh-timeoutauto, manual, neverautoInteractive timeout, Managed widgets only
retryauto, neverautoFailure to obtain a token
retry-intervalPositive integer under 900000 (ms)8000Spacing 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.

js
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 just success. timeout-or-duplicate and invalid-input-response point 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.

js
// 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