El token de Cloudflare Turnstile trae un cronómetro incorporado y, cuando se agota, el sitio lo rechaza sin más. Si recibes un cf-turnstile-response válido pero el envío devuelve un 403 aparentemente aleatorio, casi siempre estás perdiendo una carrera contra ese reloj:
- El token vive unos 300 segundos desde que Cloudflare lo emite, no desde que lo recibes tú.
- El reloj sigue corriendo mientras tu código rellena formularios o espera páginas.
- La solución: resolver justo a tiempo con CaptchaAI y enviar el token en segundos.
Cuánto vive un token de Turnstile
Un token de Turnstile expira unos 300 segundos (5 minutos) después de generarse. Es un margen holgado comparado con otros tipos, pero las condiciones de carrera aparecen igual en cuanto el workflow se complica:
- Cloudflare Turnstile: ~300 s (5 minutos).
- reCAPTCHA v2/v3: ~120 s.
- hCaptcha: ~120 s.
El cronómetro arranca cuando Cloudflare genera el token, no cuando CaptchaAI te lo devuelve.
Dónde arranca el reloj: la condición de carrera
Time 0:00 — You submit a Turnstile task to CaptchaAI
Time 0:15 — CaptchaAI begins solving
Time 0:20 — Token is generated (timer starts here)
Time 0:25 — CaptchaAI returns token to you
Time 0:25+ — Your code processes the token
Time ??? — Your code submits the token to the site
El reloj corre desde el instante 0:20: tienes hasta las 5:20 para enviar el token. Parece de sobra, hasta que miras un flujo real:
Time 0:20 — Token generated
Time 0:25 — Received by your code
Time 0:30 — Fill form fields
Time 0:35 — Navigate to next page
Time 1:00 — Handle additional dialogs
Time 2:00 — Wait for page load
Time 4:00 — Network latency spike
Time 5:30 — Submit token → EXPIRED
Entre carga de páginas, diálogos y un pico de latencia, el token caduca antes de tocar el servidor.
Patrones habituales que dejan caducar el token
Formularios de varios pasos. Obligan a recorrer varias páginas antes del envío final:
Step 1: Fill personal info → Step 2: Fill address →
Step 3: Solve CAPTCHA → Step 4: Review → Step 5: Submit
Si el CAPTCHA está en el paso 3 pero el envío ocurre en el paso 5, el retraso puede superar los 5 minutos. Es el caso de los portales de cita previa y los centros de visado BLS (España y Latinoamérica): pantallas encadenadas con el CAPTCHA a mitad del recorrido. En tu QA, resuélvelo lo más cerca posible del envío.
Colas de procesamiento por lotes. Resolver los tokens por adelantado para usarlos más tarde:
# DON'T: Solve all tokens first, then use them
tokens = []
for url in urls:
tokens.append(solve_turnstile(url)) # Tokens age while waiting
for url, token in zip(urls, tokens):
submit_form(url, token) # Early tokens may be expired
Los primeros tokens envejecen mientras el bucle procesa el resto; cuando les llega el turno, ya han caducado.
Bucles de reintento con tokens ya usados. Reutilizar un token después de un envío fallido:
token = solve_turnstile(site_key, page_url)
for attempt in range(3):
result = submit_form(page_url, token)
if result.ok:
break
# BUG: Retrying with the same token — it may be expired OR already consumed
El reintento ingenuo choca con dos límites a la vez:
- El token ya pudo caducar por tiempo.
- Turnstile lo marca como consumido tras el primer envío.
Cómo evitar la caducidad
Tres tácticas, de la más simple a la más robusta:
- Resolver justo a tiempo, en el último paso del flujo.
- Controlar la antigüedad del token antes de enviarlo.
- Pedir un token nuevo en cada reintento.
Estrategia 1: resolver justo a tiempo
Pide el token solo cuando ya estés listo para enviarlo:
import requests
import time
def solve_turnstile(site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": "YOUR_API_KEY",
"method": "turnstile",
"sitekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("Solve timed out")
# Complete all form steps FIRST
fill_personal_info()
fill_address()
navigate_to_review()
# THEN solve and submit immediately
token = solve_turnstile(site_key, page_url)
submit_form(token) # Submit within seconds of receiving the token
Deja la resolución del CAPTCHA para el final. Como CaptchaAI suele resolver un Turnstile en menos de 10 segundos, entre recibirlo y enviarlo pasan pocos segundos, lejos del límite de 300.
Estrategia 2: controlar la antigüedad del token
import time
class TimedToken:
def __init__(self, token, created_at=None):
self.token = token
self.created_at = created_at or time.time()
self.max_age = 270 # 4.5 min — safety margin from 5 min limit
@property
def is_valid(self):
return (time.time() - self.created_at) < self.max_age
@property
def remaining_seconds(self):
return max(0, self.max_age - (time.time() - self.created_at))
# Usage
timed_token = TimedToken(solve_turnstile(site_key, page_url))
# Check before using
if timed_token.is_valid:
submit_form(timed_token.token)
else:
# Solve a fresh token
timed_token = TimedToken(solve_turnstile(site_key, page_url))
submit_form(timed_token.token)
La clase marca el token con su hora y usa un tope de 270 segundos (4,5 minutos) como colchón. Antes de enviar compruebas is_valid; si ya está viejo, resuelves uno nuevo.
Estrategia 3: token nuevo en cada reintento (JavaScript)
async function submitWithFreshToken(siteKey, pageUrl, formData) {
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
// Always solve a fresh token for each attempt
const token = await solveTurnstile(siteKey, pageUrl);
const response = await fetch(pageUrl, {
method: 'POST',
body: JSON.stringify({ ...formData, 'cf-turnstile-response': token }),
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) return await response.json();
console.log(`Attempt ${attempt + 1} failed, solving fresh token...`);
}
throw new Error('All attempts failed');
}
La regla de oro para los reintentos: nunca reutilices un token. Cada vuelta del bucle resuelve un cf-turnstile-response nuevo, a salvo de la caducidad y del estado de consumido.
Señales de un token caducado
El sitio casi nunca te dice "token caducado" de forma explícita. Estas pistas lo delatan:
- HTTP 403 tras enviar el token: token no válido o caducado.
- Redirección de vuelta al formulario: falló la verificación.
- Mensaje "verificación fallida": fallo genérico que puede ser caducidad.
- Reaparece el challenge: Cloudflare rechazó el token y vuelve a desafiar.
Registro para diagnóstico
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("turnstile")
token_received_at = time.time()
token = solve_turnstile(site_key, page_url)
logger.info(f"Token received, length: {len(token)}")
# ... workflow steps ...
submit_time = time.time()
age = submit_time - token_received_at
logger.info(f"Submitting token, age: {age:.1f}s")
if age > 270:
logger.warning(f"Token may be expired (age: {age:.1f}s > 270s safety limit)")
Si los fallos coinciden con edades por encima de 270 s, la causa es la caducidad.
Actualización automática del widget en el navegador
En los flujos con navegador, el widget de Turnstile refresca los tokens por sí solo antes de que caduquen. El callback data-expired-callback se dispara cuando un token expira:
turnstile.render('#captcha', {
sitekey: '0x4AAAA...',
callback: (token) => {
console.log('New token:', token);
},
'expired-callback': () => {
console.log('Token expired — widget will auto-refresh');
}
});
Sin navegador (solo API) no hay refresco automático: la frescura del token queda entera en tus manos.
Preguntas frecuentes
¿Por qué mi token funciona en local pero falla en producción?
Porque en producción el flujo tarda más y consume el margen del token:
- carga de páginas y recursos pesados,
- diálogos intermedios,
- picos de latencia de red.
Cualquiera de ellos puede empujar el flujo más allá de los 300 segundos. Resuelve en el último paso.
¿Cuánto tarda CaptchaAI en resolver un Turnstile?
Normalmente menos de 10 segundos, con una alta tasa de éxito en los tipos compatibles. Ese margen deja casi intactos los 300 segundos de vida del token.
¿Un 403 tras enviar el token siempre significa caducidad?
No siempre. Un 403 también puede venir de:
- un token ya consumido,
- un
sitekeyincorrecto, - una
pageurlque no coincide con la del widget.
Registra la antigüedad del token: si supera los 270 s, sospecha de la caducidad.
Artículos relacionados
- Cloudflare Challenge frente a Turnstile: cómo distinguirlos
- Turnstile devuelve 403 tras enviar el token: cómo solucionarlo
- Errores frecuentes de Cloudflare Turnstile y cómo resolverlos
Evita los errores por token caducado en Turnstile: consigue tu clave API de CaptchaAI y resuelve justo a tiempo, con una alta tasa de éxito en los tipos compatibles.