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.