reCAPTCHA Token Expiration: How Long Tokens Stay Valid

reCAPTCHA token expiration is two minutes. Google’s documentation puts it plainly: each response token is valid for two minutes and can only be verified once. Miss either half and your server gets back the same unhelpful error. This post covers the three separate clocks that people mistake for one, where those 120 seconds actually go in an automated flow, and the ordering change that fixes almost every expired-token bug.
Two minutes, and exactly one verification
The rule has two halves and both of them bite.
Two minutes. The clock starts when the token is issued, not when your form is submitted. A token that sits in a hidden field while a user finishes typing is already spending its budget.
One verification. Sending the same token to Google twice fails the second time, even one second later. This is deliberate: it is what stops a captured token being replayed. If your backend verifies once in a middleware and again in the handler, the second call fails and the bug looks intermittent.
Both failures return the same thing. Google’s verification response comes back with success set to false and the error code timeout-or-duplicate, which means the response is either too old or has been used before. It does not tell you which, so treat them as one bug class and check both.
Three clocks, not one
Most confusion here comes from collapsing three different timers into a single “token expiry” idea. They are separate and they expire independently.
| Clock | Length | What happens when it runs out |
|---|---|---|
| The widget’s own response, v2 checkbox | 2 minutes | The widget clears itself and fires the expired callback. The hidden field goes empty |
| The response token, server side | 2 minutes | Verification returns timeout-or-duplicate |
| Your session on the target site | Site specific | Unrelated to reCAPTCHA. A fresh token will not fix a dead session |
The first one is the one people never see coming, because it is client side and silent. On a v2 checkbox the widget invalidates its own answer after two minutes and calls whatever function you registered as the expired callback. If you registered nothing, the ticked box stays ticked on screen while the hidden field behind it is empty, so the form posts with no token at all and the server reports a missing-input error rather than an expired one.
<!-- Register the callback. Without it the box looks ticked
while the value behind it is already gone. -->
<div class="g-recaptcha"
data-sitekey="YOUR_SITEKEY"
data-callback="onSolved"
data-expired-callback="onExpired"></div>
<script>
function onExpired() {
// Reset the widget and re-enable whatever you disabled.
grecaptcha.reset();
}
</script>How long do other challenges last?
Two minutes is not universal. If you handle more than one challenge type, the budgets differ enough to matter.
| Challenge | Token valid for | Reusable |
|---|---|---|
| reCAPTCHA v2, checkbox and Invisible | 2 minutes | No |
| reCAPTCHA v3 | 2 minutes | No |
| reCAPTCHA Enterprise | 2 minutes | No |
| Cloudflare Turnstile | 5 minutes | No |
| GeeTest v3 | Post it back immediately | No |
Turnstile is the generous one. Cloudflare’s server-side validation guide gives a token 300 seconds and rejects a replayed one with the same timeout-or-duplicate code Google uses. GeeTest v3 works the other way round: the challenge value you feed into the solve is single use and expires in about a minute, so the deadline is before the solve rather than after it. Fetch it immediately before solving, never at the top of a script.
Where the two minutes actually goes
In a hand-driven form the budget is huge. In an automated flow it is tighter than it looks, because solving is not instant.
| Step | Typical time |
|---|---|
| Load the page and read the sitekey | 1 to 3 seconds |
| Solve a reCAPTCHA v2 | 15 to 20 seconds |
| Solve a reCAPTCHA v3 | 10 to 15 seconds |
| Inject the token and submit | under a second |
| Left over | roughly 95 seconds |
Ninety-five seconds of slack is comfortable until something else sits in the middle. The usual culprits are a proxy handshake, a login step inserted between the solve and the submit, a rate limiter that sleeps your worker, or a queue that batches solved tokens for later use. That last one never works: a token is a perishable, not a resource you can pool.
Grab the token last
The fix for nearly every expired-token bug is ordering. Do everything slow first, and take the token as the final step before the request that consumes it.
# pip install capskip
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
# Slow things first: log in, warm the session, pick up cookies.
session = build_session()
sitekey = read_sitekey(session, PAGE_URL)
# Then solve, so the clock starts as late as possible.
result = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
# And submit straight away. Nothing goes between these two lines.
session.post(PAGE_URL, data={"g-recaptcha-response": result["code"]})Two rules follow from this, and they cover most of the rest.
- Never cache a token. Not in Redis, not in a variable that outlives the request, not across a retry. Solve again instead
- Never verify twice. Verify in exactly one place. If a middleware already checked the token, the route handler must read that result rather than calling Google again
Retries deserve a word of their own. If the submit fails and you retry it, the token you already sent is spent, so the retry needs a new solve. Retrying the whole unit of work is correct. Retrying only the HTTP call with the old token gives you timeout-or-duplicate every time and looks like the solver returning bad answers.
Latency, and where the solver runs
Because CapSkip runs on your own hardware, none of the two-minute budget is spent reaching a third-party endpoint across the internet. Local mode listens on 127.0.0.1 and the call never leaves the machine.
Server mode changes that slightly and it is worth knowing by how much. Pointing your workers at a shared solver on your network or on a VPS with a public IP adds one network hop per call, which is milliseconds on a LAN and tens of milliseconds to a VPS in the same region. Against 120 seconds it is noise, and it buys you one solver serving a whole fleet. Both modes are configured in the connection settings, and a static public IP is recommended for the server case.
One CapSkip detail belongs here too: a solved result is readable once. Polling the same job id a second time will not return the answer again, so store the token when you first read it rather than re-fetching it later.
FAQ
Can I extend the two minutes?
No. The window is enforced by Google and there is no site setting, parameter or plan that changes it. The only lever you have is doing less between the solve and the submit.
Does a v3 token expire faster because of the score?
No. v3 tokens get the same two minutes as v2. The score is a separate thing entirely: it describes how the traffic looked, not how long the answer lasts, and it does not decay while the token sits unused. Note that CapSkip has no minimum-score option, so nothing on the solving side is tied to it either.
My token works locally and expires in production. Why?
Almost always a queue. Local runs go straight from solve to submit, while production puts the job through a broker, a worker pool or a rate limiter first. Measure the gap between the two timestamps in production and you will usually find it is over 120 seconds. Move the solve to the worker that does the submitting.
Is timeout-or-duplicate ever the site’s fault?
Sometimes. A page that verifies the token itself before forwarding your request will consume it, so your own verification then fails as a duplicate. Proxy layers and web application firewalls occasionally do the same. If a token fails on first use with a clean timestamp, suspect something upstream of you spent it.
The short version
Two minutes, one verification, and three separate clocks that people mistake for one. Register the expired callback so the browser case is visible, solve as late as you can, and never cache or replay a token. Since a re-solve is free on a local captcha solver, solving again is always the right answer to an expired token. For the mechanics of each version, see the reCAPTCHA v2 guide and the v3 guide, the Turnstile page for the five-minute case, and what reCAPTCHA is for the background.
