Troubleshooting

reCAPTCHA Token Expiration: Timing Windows and Race Conditions

The timeout-or-duplicate error is the most common reCAPTCHA failure in automation workflows. It means your token expired before submission or was used twice. With reCAPTCHA tokens lasting only 120 seconds and API solvers taking 15-45 seconds to generate them, timing management is critical. This guide covers the exact expiration mechanics, common race conditions, and practical timing strategies.


The 120-second rule

Every reCAPTCHA token expires exactly 120 seconds after generation. This applies to all versions:

Token generated (solve complete)
    ├─── 0s:   Token is valid ✓
    ├─── 60s:  Token is valid ✓
    ├─── 110s: Token is valid ✓  (but cutting it close)
    ├─── 119s: Token is valid ✓  (dangerous territory)
    └─── 120s: Token EXPIRED ✗  (timeout-or-duplicate)

Where the timer starts

Scenario Timer starts when...
reCAPTCHA v2 checkbox User checks the checkbox (challenge solved)
reCAPTCHA v2 image grid User completes final image selection round
reCAPTCHA v2 invisible execute() callback fires with token
reCAPTCHA v3 execute() Promise resolves with token
API solver (CaptchaAI) Solver generates the token (NOT when you receive it)

The solver timing gap

When using an API solver, there is a delay between when the solver generates the token and when your code receives it:

Solver generates token (timer starts)
    ↓ ~1-5 seconds (network + polling interval)
Your code receives token via res.php poll
    ↓ You now have ~115-119 seconds remaining
Your code processes and submits token
    ↓ ~1-10 seconds (depends on your workflow)
Target website validates token with Google
    ↓ ~1-2 seconds (Google API response time)
Total remaining after validation: ~105-117 seconds (comfortable)

In practice, the 120-second window provides ample time when your code submits immediately after receiving the token. Problems occur when:

  • Your code has additional processing steps between receiving the token and submitting it
  • Multiple forms need to be filled before submission
  • The target website has a slow response time
  • You queue tokens and submit them later

Common race conditions

Race condition 1: Parallel solve + sequential submit

# WRONG: Solving multiple CAPTCHAs in parallel, then submitting sequentially
tokens = []
for url in urls:
    task_id = solve_captcha(url)  # All submitted at t=0
    tokens.append(task_id)

# All tokens arrive around t=30
solved_tokens = [poll_result(tid) for tid in tokens]

# Sequential submission: first token at t=32, last at t=120+
for i, (url, token) in enumerate(zip(urls, solved_tokens)):
    submit_form(url, token)  # Later tokens may be expired!
    time.sleep(10)  # Each wait adds pressure

Fix: Solve and submit each CAPTCHA before starting the next:

# CORRECT: Solve and submit one at a time
for url in urls:
    token = solve_and_wait(url)  # Token received at t=30
    submit_form(url, token)      # Submitted at t=31 (89 seconds remaining)

Race condition 2: Token pre-fetching

# WRONG: Pre-fetching tokens before knowing when they'll be used
token = solve_captcha()  # Token received at t=0

# ... user fills out form (30-120+ seconds) ...
# ... validation checks ...
# ... other processing ...

submit_form(token)  # Token may be expired!

Fix: Solve the CAPTCHA as the last step before submission:

# CORRECT: Late-bind the CAPTCHA solve
prepare_form_data()   # Do everything that doesn't need the token
validate_inputs()     # Run validation before spending a token

# Now solve and submit immediately
token = solve_captcha()  # Token received at t=0
submit_form(token)       # Submitted at t=1 (119 seconds remaining)

Race condition 3: Multi-step form submission

# PROBLEMATIC: Multi-step form where CAPTCHA is on step 1 but submit is step 3
token = solve_captcha()       # t=0: Token received

fill_step_1(token)            # t=5: Step 1 submitted
response = fill_step_2()      # t=15: Step 2 completed
# ... step 2 has additional verification ...
wait_for_verification()       # t=60: Verification complete
fill_step_3_and_submit()      # t=65: Final submission (55 seconds remaining - OK)
# BUT if step 2 takes longer than expected...

Fix: Measure token age and re-solve if necessary:

token_received_at = time.time()
token = solve_captcha()
token_received_at = time.time()

# ... multi-step process ...

# Before final submission, check token age
token_age = time.time() - token_received_at
if token_age > 100:  # 20-second safety margin
    print(f"Token is {token_age:.0f}s old — requesting fresh token")
    token = solve_captcha()
    token_received_at = time.time()

submit_final(token)

Token timing manager

A production-ready class for managing token lifetimes:

import time
import requests

class TokenTimingManager:
    """Manage reCAPTCHA token timing to prevent expiration errors."""

    API_KEY = "YOUR_API_KEY"
    TOKEN_LIFETIME = 120
    SAFETY_MARGIN = 15  # seconds before expiry to consider "stale"

    def __init__(self, site_key, page_url, version="v2"):
        self.site_key = site_key
        self.page_url = page_url
        self.version = version
        self.current_token = None
        self.token_timestamp = None

    def _solve(self):
        """Request and poll for a new token."""
        params = {
            "key": self.API_KEY,
            "method": "userrecaptcha",
            "googlekey": self.site_key,
            "pageurl": self.page_url,
            "json": 1,
        }
        if self.version == "v3":
            params.update({"version": "v3", "action": "submit", "min_score": "0.9"})

        submit = requests.post("https://ocr.captchaai.com/in.php", data=params).json()
        task_id = submit["request"]

        for _ in range(60):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1,
            }).json()

            if result.get("status") == 1:
                self.current_token = result["request"]
                self.token_timestamp = time.time()
                return self.current_token

        raise TimeoutError("Token solve timeout")

    @property
    def token_age(self):
        """Seconds since current token was received."""
        if self.token_timestamp is None:
            return float("inf")
        return time.time() - self.token_timestamp

    @property
    def token_remaining(self):
        """Seconds remaining before token expires."""
        return max(0, self.TOKEN_LIFETIME - self.token_age)

    @property
    def is_fresh(self):
        """Whether the token is fresh enough to use."""
        return self.token_remaining > self.SAFETY_MARGIN

    def get_token(self):
        """Get a valid token, solving if current is stale or missing."""
        if self.current_token and self.is_fresh:
            return self.current_token

        return self._solve()

    def use_token(self):
        """Get and consume a token (cannot be reused)."""
        token = self.get_token()
        # Mark as consumed
        self.current_token = None
        self.token_timestamp = None
        return token


# Usage
manager = TokenTimingManager(
    site_key="6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    page_url="https://example.com/login",
)

# Get a fresh token right before submission
token = manager.use_token()
print(f"Token remaining: {manager.TOKEN_LIFETIME}s (fresh solve)")

# Submit form with token...

Handling the timeout-or-duplicate error

When you receive timeout-or-duplicate, diagnose the cause:

def handle_recaptcha_error(error_codes, token_age_seconds):
    """Diagnose and handle reCAPTCHA validation errors."""

    if "timeout-or-duplicate" in error_codes:
        if token_age_seconds > 120:
            return {
                "cause": "Token expired (age: {:.0f}s > 120s)".format(token_age_seconds),
                "fix": "Reduce time between receiving and submitting token",
                "action": "re-solve",
            }
        elif token_age_seconds < 5:
            return {
                "cause": "Token likely reused (duplicate submission)",
                "fix": "Ensure each form submission gets a unique token",
                "action": "re-solve",
            }
        else:
            return {
                "cause": "Token may have been reused or server-side timing issue",
                "fix": "Check for double-submit in form handler",
                "action": "re-solve",
            }

    if "invalid-input-response" in error_codes:
        return {
            "cause": "Token is malformed or corrupted",
            "fix": "Check token transmission (URL encoding, field name)",
            "action": "re-solve",
        }

    return {"cause": "Unknown", "action": "investigate"}

Timing best practices

Practice Recommendation
Solve-to-submit gap Keep under 90 seconds (30-second margin)
Poll interval 5 seconds between res.php checks
Pre-solve Only pre-solve if you know submission will happen within 60 seconds
Retry on expiry Always request a fresh token on timeout-or-duplicate
Token queue Never queue tokens — each expires independently
Parallel operations Solve-per-task, not solve-all-then-use
Monitoring Track token age at submission time

Frequently asked questions

Can I extend the 120-second token window?

No. The expiration is enforced server-side by Google and cannot be extended by any means. The 120 seconds is a hard limit.

Does polling delay reduce my usable token time?

Yes, slightly. If you poll every 5 seconds, you may receive the token up to 5 seconds after it was generated. In practice, this means you have ~115 seconds instead of 120. This is usually not a problem.

Should I pre-solve tokens to have them ready?

Only if you can guarantee the token will be submitted within 60-90 seconds. Pre-solving is useful for latency-sensitive workflows (e.g., competitive purchasing) where the extra 15-45 seconds of solve time matters. For most workflows, solve-on-demand is safer.

Why does Google use the same error for expired and duplicate tokens?

Google intentionally obscures the distinction to prevent automation scripts from using the error code to detect which case occurred. Both cases have the same remedy: request a new token.


Summary

reCAPTCHA tokens expire after exactly 120 seconds and can only be used once. The most common automation error — timeout-or-duplicate — is caused by submitting tokens too late or reusing them. When using CaptchaAI, solve tokens just before submission, track token age, and never queue or reuse tokens. Use the timing manager pattern above to automatically handle expiration and re-solving in production workflows.

Discussions (0)

No comments yet.

Related Posts

Reference CAPTCHA Token Injection Methods Reference
Complete reference for injecting solved CAPTCHA tokens into web pages.

Complete reference for injecting solved CAPTCHA tokens into web pages. Covers re CAPTCHA, Turnstile, and Cloud...

Python Automation Cloudflare Turnstile
Apr 08, 2026
Explainers reCAPTCHA v2 Invisible: Trigger Detection and Solving
Detect and solve re CAPTCHA v 2 Invisible challenges with Captcha AI — identify triggers, extract parameters, and handle auto-invoked CAPTCHAs.

Detect and solve re CAPTCHA v 2 Invisible challenges with Captcha AI — identify triggers, extract parameters,...

Python Automation reCAPTCHA v2
Apr 07, 2026
Tutorials Pytest Fixtures for CaptchaAI API Testing
Build reusable pytest fixtures to test CAPTCHA-solving workflows with Captcha AI.

Build reusable pytest fixtures to test CAPTCHA-solving workflows with Captcha AI. Covers mocking, live integra...

Python Automation Cloudflare Turnstile
Apr 08, 2026
API Tutorials How to Solve reCAPTCHA v2 Enterprise with Python
Solve re CAPTCHA v 2 Enterprise using Python and Captcha AI API.

Solve re CAPTCHA v 2 Enterprise using Python and Captcha AI API. Complete guide with sitekey extraction, task...

Python Automation reCAPTCHA v2
Apr 08, 2026
API Tutorials Solving CAPTCHAs with Swift and CaptchaAI API
Complete guide to solving re CAPTCHA, Turnstile, and image CAPTCHAs in Swift using Captcha AI's HTTP API with URLSession, async/await, and Alamofire.

Complete guide to solving re CAPTCHA, Turnstile, and image CAPTCHAs in Swift using Captcha AI's HTTP API with...

Automation Cloudflare Turnstile reCAPTCHA v2
Apr 05, 2026
Troubleshooting ERROR_PAGEURL: URL Mismatch Troubleshooting Guide
Fix ERROR_PAGEURL when using Captcha AI.

Fix ERROR_PAGEURL when using Captcha AI. Diagnose URL mismatch issues, handle redirects, SPAs, and dynamic URL...

Python Automation Cloudflare Turnstile
Mar 23, 2026
API Tutorials How to Solve reCAPTCHA v2 Callback Using API
how to solve re CAPTCHA v 2 callback implementations using Captcha AI API.

Learn how to solve re CAPTCHA v 2 callback implementations using Captcha AI API. Detect the callback function,...

Automation reCAPTCHA v2 Webhooks
Mar 01, 2026
Integrations Scrapy + CaptchaAI Integration Guide
Integrate Captcha AI into Scrapy spiders to automatically solve CAPTCHAs during web crawling with middleware and signal handlers.

Integrate Captcha AI into Scrapy spiders to automatically solve CAPTCHAs during web crawling with middleware a...

Automation reCAPTCHA v2 Scrapy
Jan 27, 2026
Tutorials CAPTCHA Solving Fallback Chains
Implement fallback chains for CAPTCHA solving with Captcha AI.

Implement fallback chains for CAPTCHA solving with Captcha AI. Cascade through solver methods, proxy pools, an...

Python Automation Cloudflare Turnstile
Apr 06, 2026
Troubleshooting Handling reCAPTCHA v2 and Cloudflare Turnstile on the Same Site
Solve both re CAPTCHA v 2 and Cloudflare Turnstile on sites that use multiple CAPTCHA providers — detect which type appears, solve each correctly, and handle pr...

Solve both re CAPTCHA v 2 and Cloudflare Turnstile on sites that use multiple CAPTCHA providers — detect which...

Python Automation Cloudflare Turnstile
Mar 23, 2026
Troubleshooting Turnstile Token Invalid After Solving: Diagnosis and Fixes
Fix Cloudflare Turnstile tokens that come back invalid after solving with Captcha AI.

Fix Cloudflare Turnstile tokens that come back invalid after solving with Captcha AI. Covers token expiry, sit...

Python Cloudflare Turnstile Web Scraping
Apr 08, 2026
Troubleshooting GeeTest v3 Error Codes: Complete Troubleshooting Reference
Complete reference for Gee Test v 3 error codes — from registration failures to validation errors — with causes, fixes, and Captcha AI-specific troubleshooting.

Complete reference for Gee Test v 3 error codes — from registration failures to validation errors — with cause...

Automation Testing GeeTest v3
Apr 08, 2026
Troubleshooting Cloudflare Turnstile 403 After Token Submission: Fix Guide
Fix 403 errors after submitting Cloudflare Turnstile tokens.

Fix 403 errors after submitting Cloudflare Turnstile tokens. Debug cookie handling, request headers, token inj...

Python Cloudflare Turnstile Web Scraping
Feb 24, 2026