Troubleshooting

Expiração do token reCAPTCHA: janelas de tempo e condições de corrida

Se o log mostra timeout-or-duplicate depois de um envio aparentemente correto, o problema raramente é a chave de API ou a sitekey. O token do reCAPTCHA vale 120 segundos a partir do instante em que é gerado e aceita uma única validação: fora da janela, ou repetido, ele devolve esse código.

A pegadinha é onde o cronômetro começa: não quando seu código recebe o token, e sim quando o desafio termina do outro lado. Polling, processamento e latência saem do mesmo orçamento.


Onde o cronômetro de 120 segundos começa a contar

A expiração é imposta no servidor do Google e vale para todas as variantes:

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)

O ponto de partida depende de como o token foi produzido:

Cenário O cronômetro começa quando...
Caixa de seleção do reCAPTCHA v2 O usuário marca a caixa (desafio resolvido)
Grade de imagens do reCAPTCHA v2 O usuário conclui a última rodada de seleção
reCAPTCHA v2 invisível O callback de execute() dispara com o token
reCAPTCHA v3 A Promise de execute() resolve com o token
Solucionador de API (CaptchaAI) O solucionador gera o token (NÃO quando você o recebe)

O tempo que se perde entre resolver e receber

Repare na última linha: com um solucionador de API, o relógio corre antes de o token chegar ao seu processo.

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)

Enviando logo depois de receber, sobram mais de 100 segundos. O orçamento aperta quando:

  • há processamento entre receber e enviar o token;
  • outras telas vêm antes do envio final;
  • o site de destino responde devagar;
  • os tokens esperam em uma fila.

Caso real: um checkout de QA em três telas

Uma suíte de integração roda contra o staging da equipe (https://staging.example.com/qa-checkout): reCAPTCHA v2 na primeira de três telas, envio real na terceira. Local passava; o CI falhava de forma intermitente. O culpado era um sleep fixo de 90 segundos simulando a conferência de dados: com os ~30 s da resolução, o token chegava ao POST final com 125 segundos. Resolver logo antes do envio corrigiu.

A geografia pesa: um worker em sa-east-1 (São Paulo) contra um endpoint europeu paga um RTT que Frankfurt não paga. Pela LGPD, no log basta a idade do token e o código de erro.

Margens que funcionam na prática

Prática Recomendação
Resolver → enviar Abaixo de 90 s (margem de 30 s)
Polling 5 s entre consultas ao res.php
Resolver antecipadamente Só se o envio ocorrer em até 60 s
Na expiração Sempre peça token novo
Fila de tokens Nunca — cada um expira sozinho
Operações paralelas Um ciclo resolver-enviar por tarefa
Monitoramento Registre a idade do token no envio

Nada disso se resolve trocando de plano. A CaptchaAI cobra por thread simultânea, com resoluções ilimitadas: o BASIC (US$ 15/mês, 5 threads) sustenta cinco fluxos simultâneos e o ADVANCE (US$ 90/mês, 50 threads) cobre CI maiores. Token vencido é desenho de fluxo, não falta de thread.


As três condições de corrida a seguir produzem a mesma assinatura no log, com correções diferentes.

Corrida 1: resolver tudo em paralelo e enviar em série

# 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

Correção: feche um ciclo de resolver-enviar por vez antes de abrir o próximo.

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

Corrida 2: pedir o token cedo demais

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

Correção: deixe a resolução para a última etapa antes do envio (late binding).

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

Corrida 3: formulário dividido em várias etapas

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

Correção: meça a idade do token antes do envio final e peça outro se já estiver velho.

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)

Uma classe para controlar a idade do token

O padrão abaixo concentra idade, margem e uso único: envia a tarefa a in.php, consulta res.php e guarda o instante da resolução.

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", "score_qa": "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://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...

Repare no use_token(): ele zera o token ao devolvê-lo, o que impede o reenvio do mesmo valor — a metade "duplicate" do erro.


Como separar "expirou" de "já foi usado"

O Google devolve o mesmo código nos dois casos, de propósito. A informação que falta está do seu lado: a idade do token no POST. Com ela no log, o diagnóstico vira uma árvore curta.

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

Como ler o resultado

Idade acima de 120 s aponta latência no fluxo; perto de zero, envio duplicado — clique duplo, retry do cliente HTTP ou redirect.

Quatro perguntas antes de mexer no código

  • A idade do token está no log de cada POST?
  • O botão de envio é desabilitado no primeiro clique?
  • O cliente HTTP repete a requisição sozinho?
  • A pageurl é idêntica à URL real do formulário?

Perguntas frequentes

Qual idade máxima é segura para enviar um token?

Trate 90 segundos como teto operacional. O limite duro é 120 s, mas o trecho entre o POST e a validação junto ao Google não é seu: a margem de 30 s cobre isso.

Recarregar a página no Selenium invalida o token?

Sim. Cada carregamento monta um widget novo e o token anterior deixa de valer. Resolva após o carregamento final.

Posso usar o mesmo token em dois formulários do mesmo site?

Não. O token é de uso único e fica amarrado à sitekey e à pageurl da resolução. O segundo envio cai na metade "duplicate" do erro, mesmo dentro dos 120 s.

Um plano maior aumenta a validade do token?

Não. As threads definem quantas resoluções rodam em paralelo, não quanto tempo o token vive: do BASIC (US$ 15/mês, 5 threads) ao VIP-3 (US$ 7.500/mês, 5.000 threads), a janela é a mesma. Os 120 s são regra do Google.


O essencial

O token dura 120 segundos e vale uma validação. O timeout-or-duplicate conta uma de duas histórias: envelheceu no caminho ou foi enviado duas vezes. Resolva perto do envio, registre a idade em cada POST e nunca enfileire tokens — o gerenciador acima cobre os dois casos na CaptchaAI.

Artigos relacionados

Os comentários estão desativados para este artigo.