Skip to content

Cloudflare Turnstile

Cloudflare Turnstile Token Expired? Lifetime and Reuse Explained

Cloudflare Turnstile tokens last 300 seconds and work once. Why tokens expire, what timeout-or-duplicate means, and how to use each token before it runs out.

3 min readPublished Updated

A Cloudflare Turnstile token has two limits: it expires 300 seconds after it is issued, and it can be verified once. Most “the site rejected my token” problems come down to one of the two. This guide explains both, the error the site sees, and how to structure your code so a token is always used while it is fresh.

The two rules

  1. Lifetime. A token is valid for 300 seconds, five minutes, from the moment the widget or a solver obtains it. The clock does not start when you receive it, and it does not pause while your job waits in a queue.
  2. Single use. The site’s server redeems a token by sending it to Cloudflare’s siteverify endpoint. The first call succeeds; any later call with the same token fails.

When either rule is broken, siteverify answers with the error code timeout-or-duplicate. The site then treats the submission as if the widget had not been solved, which usually looks like a generic “please try again” message.

How ZeroCaptcha shows the lifetime

Every solved task tells you when its token expires, so you never have to guess. In the compatible format, getTaskResult answers:

{
"errorId": 0,
"taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b",
"status": "ready",
"solution": { "token": "0.xT4…", "type": "turnstile" },
"cost": "0.000800",
"createTime": 1790762400,
"endTime": 1790762404,
"solveCount": 1,
"expiresAt": "2026-09-30T10:05:04Z"
}

cost is what the task cost in US dollars, and expiresAt the moment the token stops working, in UTC. A REST task shows the same as tokenIssuedAt and tokenExpiresAt. If you read a solved task after its token has expired, getTaskResult answers ERROR_TOKEN_EXPIRED: the task was solved and charged, and the token can no longer be used. See the errors reference.

Patterns that waste tokens

  • Solving ahead. Creating tasks at the start of a batch and submitting forms minutes later. By the time the fifth form is sent, its token may be old.
  • Retrying the form with the same token. If the site answers with an error for another reason, such as a wrong password, the token has already been redeemed. The next attempt needs a new token.
  • Sharing a token between requests. One token, one form submission.
  • Slow polling. Asking for the result every 30 seconds adds up to half a minute of the token’s life. Poll every one or two seconds, or use a callback so the result is pushed to you.

A pattern that works

Create the task at the moment your code reaches the form, wait for the token, and submit straight away.

import os, time, uuid, requests
API, KEY = os.environ["ZEROCAPTCHA_API"], os.environ["ZEROCAPTCHA_KEY"]
def fresh_token(page_url: str, sitekey: str, action: str = "", cdata: str = "") -> str:
task = {"type": "TurnstileTaskProxyless", "websiteURL": page_url, "websiteKey": sitekey}
# The widget's data-action and data-cdata (or turnstile.render()'s action and cData), sent
# only when it sets them: many sites check both when they verify the token.
task["metadata"] = {name: value for name, value in (("action", action), ("cdata", cdata)) if value}
# One Idempotency-Key per task: a retried create with it returns the same task.
created = requests.post(f"{API}/createTask", timeout=15, headers={"Idempotency-Key": str(uuid.uuid4())},
json={"clientKey": KEY, "task": task}).json()
if created["errorId"]:
raise RuntimeError(created["errorCode"])
deadline = time.monotonic() + 180
while time.monotonic() < deadline:
time.sleep(2)
result = requests.post(f"{API}/getTaskResult", timeout=15,
json={"clientKey": KEY, "taskId": created["taskId"]}).json()
if result["errorId"]:
raise RuntimeError(result["errorCode"])
if result["status"] == "ready":
return result["solution"]["token"]
raise TimeoutError("no token within 180 seconds")
token = fresh_token("https://shop.example.com/login", "0x4AAAAAAAB1cD2eF3gH4iJ5",
action="login", cdata="session-7f3a9c2e")
# Submit the form now, with token as cf-turnstile-response.

If the form submission fails for a reason other than the token, call fresh_token again before the next attempt. Submit a Cloudflare Turnstile token shows how the token goes into the form.

Budget the five minutes

Measure the time from expiresAt minus 300 seconds (when the token was issued) to your form submission. If it is often more than a minute, something in your pipeline is holding tokens: a queue, a batch step, or a slow page load after the solve. The median solve time over the last 24 hours is on the status page, which helps you size deadlines.

Cost

A task is charged when its token is ready, not when you use it. An expired token is therefore money spent for nothing. Tasks that fail or time out are never charged, but a solved task whose token you let expire is. Using tokens promptly is the simplest way to keep the cost per useful token at the listed price.

More on the solving flow is on the Cloudflare Turnstile solver page.

Questions

How long is a Cloudflare Turnstile token valid?

300 seconds from the moment it is issued, and for one verification only. After that the site's siteverify call answers timeout-or-duplicate.

Am I charged for a token that expired before I used it?

Yes. The task succeeded, so it was charged; getTaskResult then answers ERROR_TOKEN_EXPIRED. Use each token as soon as it is ready to avoid paying for tokens you cannot use.

Can I ask for a token in advance and keep it for later?

Not for long. A token is valid for 300 seconds, so create the task when you are about to submit the form, not minutes before.

Read next

This guide is part of the Cloudflare Turnstile solver hub. Every task is charged only when a token is ready.

Get an API key