Solución de Problemas

Caducidad del token reCAPTCHA: ventanas de tiempo y condiciones de carrera

Un token de reCAPTCHA vive 120 segundos y se gasta en un único envío. El cronómetro no arranca cuando tu código recibe el token, sino cuando el desafío se resuelve al otro lado: esa diferencia separa un flujo estable de otro que devuelve timeout-or-duplicate a ratos. Si un token recién llegado es rechazado, la causa es una de dos: lo enviaste tarde o lo enviaste dos veces.

Qué significa realmente timeout-or-duplicate

Google devuelve un único código para fallos distintos, y lo hace a propósito. En la práctica son tres escenarios.

  1. Caducó: más de 120 segundos entre su generación y la validación.
  2. Ya se había usado: un doble clic, un reintento automático o un retry de tu cliente HTTP.
  3. Viajó mal: se recortó, se codificó dos veces o se envió fuera del campo g-recaptcha-response.

El remedio siempre es pedir un token nuevo, pero la causa decide qué corriges: registra su antigüedad en cada envío.

La ventana de 120 segundos, segundo a segundo

Cada token caduca exactamente 120 segundos después de generarse, sin excepciones por versión:

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)

Dónde arranca el cronómetro

Escenario El cronómetro arranca cuando...
reCAPTCHA v2 con casilla El usuario marca la casilla
reCAPTCHA v2 con cuadrícula El usuario termina la última selección
reCAPTCHA v2 invisible El callback de execute() entrega el token
reCAPTCHA v3 La promesa de execute() se resuelve
Solver por API (CaptchaAI) El solver genera el token, no cuando tú lo recibes

Esa última fila causa casi todas las sorpresas: el token envejece mientras tu bucle espera la siguiente consulta a res.php.

El desfase entre el solver y tu código

Con un solver por API hay un hueco entre la generación del token y el momento en que tu proceso lo tiene en memoria:

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)

Con un envío inmediato sobra tiempo. Los problemas llegan cuando entre la recepción y el envío se cuela trabajo extra: validaciones, formularios encadenados, un destino lento o tokens guardados en cola.

Tres condiciones de carrera que rompen el envío

1. Resolver en paralelo y enviar en serie

# 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

El lote nace a la vez y se consume en fila india: el primer token llega holgado y el último, muerto. Solución: cierra cada ciclo antes de abrir el siguiente.

# 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)

Si necesitas paralelismo, replica el ciclo completo en varios workers: cada thread lleva un CAPTCHA de principio a fin.

2. Resolver el CAPTCHA demasiado pronto

# 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!

Pedir el token "para tenerlo listo" parece una optimización y es lo contrario: gastas un thread y el reloj corre mientras el flujo avanza. Solución: resuelve en el último paso.

# 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)

3. Formularios de varios pasos

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

Aquí el flujo funciona en tu equipo y falla en producción: el paso intermedio depende de un tercero. Solución: mide la antigüedad antes del envío final y vuelve a resolver si hace falta.

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)

Un gestor de tiempos listo para producción

Esta clase aplica las tres reglas: conoce la edad del token, sabe cuándo está rancio y lo marca como consumido.

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"})

        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://staging.example.com/qa-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...

Fíjate en use_token(): al entregar el token lo borra del estado interno, una barrera barata contra la otra mitad del timeout-or-duplicate.

Diagnosticar el error en tiempo de ejecución

Con la antigüedad medida, el error deja de ser ambiguo y se convierte en una decisión programable:

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

Guarda la clasificación en tus logs: en unos días sabrás si el problema es de latencia o de reenvíos.

Un caso habitual: portales públicos con verificación lenta

Los portales de trámites públicos — cita previa, gestiones fiscales, centros de visados BLS — castigan a los tokens: el formulario ocupa varias pantallas, hay una verificación intermedia y el servidor va lento en las horas de más carga. Si tu equipo de QA resuelve el CAPTCHA en la primera pantalla, el token llega al envío final con 80 o 90 segundos encima.

Aplica la corrección de la condición de carrera 3: resuelve en el último paso y comprueba la antigüedad antes de enviar. Respeta los términos de servicio del sitio y la normativa de protección de datos.

Márgenes recomendados

  • Hueco entre resolver y enviar: por debajo de 90 segundos.
  • Intervalo de sondeo: 5 segundos entre consultas a res.php.
  • Resolver con antelación: solo si el envío llega antes de 60 segundos.
  • Reintento: token nuevo ante cada timeout-or-duplicate.
  • Colas de tokens: evítalas, cada token caduca por su cuenta.
  • Paralelismo: un ciclo completo por tarea, nunca un lote.
  • Observabilidad: registra la antigüedad del token en cada envío.

Preguntas frecuentes

¿Cómo distingo un token caducado de uno reutilizado?

Por la antigüedad. Si pasaba de 120 segundos, caducó; si tenía uno o dos, se envió dos veces. Sin ese dato en el log vas a ciegas.

¿Puedo usar el mismo token en dos formularios?

No. El token se consume en la primera validación, aunque le queden 100 segundos. Cada envío necesita su propia resolución.

¿Qué margen debería dejar entre resolver y enviar?

Al menos 30 segundos de colchón: no envíes tokens de más de 90 segundos. Con un tercero lento, baja el umbral a 60 y vuelve a resolver antes del envío final.

¿Y si es una persona quien rellena el formulario?

No resuelvas por adelantado: dispara la resolución cuando el usuario pulsa enviar, no al abrir la página. Un formulario largo se come la ventana entera.

¿Aumentar los threads de mi plan evita el timeout-or-duplicate?

No directamente. Los threads fijan cuántos CAPTCHA tienes en vuelo, no la duración del token. Ayudan si el error viene de tareas encoladas; si el retraso está en tu código, no cambian nada.

En resumen

Un token dura 120 segundos y se usa una vez: cualquier diseño que asuma otra cosa acaba en timeout-or-duplicate. Con CaptchaAI la pauta cabe en tres líneas: resuelve lo más tarde posible, envía en cuanto tengas el token y mide su antigüedad antes de confiar en él.

Artículos relacionados

Los comentarios están deshabilitados para este artículo.