¿Qué pasa cuando una tarea de resolución de CAPTCHA agota todos sus reintentos y aun así falla? Sin una red de seguridad, ese trabajo desaparece detrás de una línea de log que nadie vuelve a leer.
Una dead letter queue (DLQ) —o cola de mensajes fallidos— es el patrón que lo evita: aparta cada tarea irrecuperable para reintentarla más tarde, avisar al equipo o analizar por qué falló. En un scraper que procesa miles de páginas por hora, esa cola marca la diferencia entre un fallo silencioso y unos datos que sí puedes recuperar.
Qué tareas acaban en la DLQ
No todos los fallos merecen el mismo trato. Estas son las causas más habituales por las que una tarea CAPTCHA termina en la cola de fallos:
ERROR_CAPTCHA_UNSOLVABLE: el servicio no pudo resolver el desafío.ERROR_NO_SLOT_AVAILABLE: todos los workers estaban ocupados y se agotaron los reintentos.- Tiempo de espera agotado: el solver no devolvió un resultado dentro del plazo.
- Errores de red: la conexión se cortó durante el sondeo.
Sin una DLQ, cada uno de estos casos se queda en una línea de registro. Con ella, tienes un inventario de trabajo pendiente que puedes reprocesar en cuanto el sistema se recupere.
DLQ en memoria con reintentos en Python
La versión más sencilla vive en memoria y encaja perfecto en scripts de ejecución corta.
El modelo de datos y el flujo de reintento
Cada FailedTask guarda la sitekey, la URL, el error, el número de intentos y una marca de tiempo. La función solve_captcha reintenta con backoff exponencial y solo empuja la tarea a la DLQ cuando agota el último intento, en lugar de dejarla caer:
import time
import json
import requests
from collections import deque
from dataclasses import dataclass, asdict
from typing import Optional
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class FailedTask:
sitekey: str
page_url: str
error: str
attempts: int
timestamp: float
task_id: Optional[str] = None
class DeadLetterQueue:
def __init__(self, max_size=1000, max_retries=3):
self._queue = deque(maxlen=max_size)
self.max_retries = max_retries
def push(self, task: FailedTask):
self._queue.append(task)
print(f"[dlq] Added: {task.error} (attempts: {task.attempts})")
def pop(self) -> Optional[FailedTask]:
return self._queue.popleft() if self._queue else None
def size(self) -> int:
return len(self._queue)
def peek_all(self) -> list:
return [asdict(t) for t in self._queue]
def export_json(self, path: str):
with open(path, "w") as f:
json.dump(self.peek_all(), f, indent=2)
print(f"[dlq] Exported {self.size()} tasks to {path}")
dlq = DeadLetterQueue(max_retries=3)
def solve_captcha(sitekey, page_url, max_retries=3):
for attempt in range(max_retries + 1):
try:
resp = requests.post(SUBMIT_URL, data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}, timeout=15)
data = resp.json()
if data["status"] != 1:
raise Exception(data["request"])
task_id = data["request"]
for _ in range(24):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": "1",
}, timeout=15).json()
if poll["status"] == 1:
return poll["request"]
if poll["request"] != "CAPCHA_NOT_READY":
raise Exception(poll["request"])
raise TimeoutError(f"Task {task_id} timed out")
except Exception as e:
if attempt == max_retries:
dlq.push(FailedTask(
sitekey=sitekey,
page_url=page_url,
error=str(e),
attempts=attempt + 1,
timestamp=time.time(),
))
return None
time.sleep(2 ** attempt)
return None
# Process a batch
urls = [f"https://example.com/page/{i}" for i in range(5)]
for url in urls:
token = solve_captcha("6Le-SITEKEY", url)
if token:
print(f"Solved: {token[:40]}...")
print(f"\nDLQ size: {dlq.size()}")
Al procesar un lote de cinco páginas, la salida deja claro qué se resolvió y qué quedó apartado:
Solved: 03AGdBq26ZfPxL...
Solved: 03AGdBq27AbCdE...
[dlq] Added: ERROR_CAPTCHA_UNSOLVABLE (attempts: 4)
Solved: 03AGdBq28FgHiJ...
[dlq] Added: Task 71823460 timed out (attempts: 4)
DLQ size: 2
Cómo drenar y reintentar la DLQ
Apartar las tareas no sirve de nada si nunca vuelves a procesarlas. Esta función drena la cola y reintenta cada tarea con un presupuesto de intentos adicional. Ejecútala justo después del lote principal o de forma programada:
def retry_dlq(dlq: DeadLetterQueue, max_retries=2):
retried = 0
recovered = 0
while dlq.size() > 0:
task = dlq.pop()
if task.attempts >= dlq.max_retries + max_retries:
print(f"[dlq] Permanently failed: {task.sitekey} — {task.error}")
continue
retried += 1
token = solve_captcha(
task.sitekey, task.page_url, max_retries=max_retries
)
if token:
recovered += 1
print(f"[dlq-retry] Recovered: {token[:40]}...")
print(f"[dlq] Retried: {retried}, Recovered: {recovered}")
# Run DLQ retry after main batch
retry_dlq(dlq)
Cuándo descartar una tarea de forma definitiva
El tope dlq.max_retries + max_retries corta los reintentos infinitos: cuando una tarea supera ese total, se marca como fallo permanente y sale de la cola. Un buen punto de partida son dos o tres pasadas por la DLQ además de los reintentos originales; si una tarea acumula más de seis fallos, casi siempre es un parámetro mal configurado.
DLQ persistente en archivo con JavaScript
En un servicio de larga duración, una DLQ en memoria se pierde en cada reinicio del proceso. Esta versión en Node.js persiste la cola en un archivo JSON: cada push y cada pop se escriben en disco, así que reiniciar el proceso no borra el trabajo pendiente.
const fs = require('fs');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const DLQ_FILE = './captcha-dlq.json';
class DeadLetterQueue {
constructor(maxRetries = 3) {
this.maxRetries = maxRetries;
this.queue = this._load();
}
push(task) {
this.queue.push({
...task,
timestamp: Date.now(),
});
this._save();
console.log(`[dlq] Added: ${task.error} (attempts: ${task.attempts})`);
}
pop() {
const task = this.queue.shift();
if (task) this._save();
return task || null;
}
size() {
return this.queue.length;
}
_load() {
try {
return JSON.parse(fs.readFileSync(DLQ_FILE, 'utf8'));
} catch {
return [];
}
}
_save() {
fs.writeFileSync(DLQ_FILE, JSON.stringify(this.queue, null, 2));
}
}
const dlq = new DeadLetterQueue(3);
async function solveCaptcha(sitekey, pageurl, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: { key: API_KEY, method: 'userrecaptcha', googlekey: sitekey, pageurl, json: 1 }
});
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 }
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
}
throw new Error(`Task ${taskId} timed out`);
} catch (err) {
if (attempt === maxRetries) {
dlq.push({ sitekey, pageurl, error: err.message, attempts: attempt + 1 });
return null;
}
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
}
// Process tasks
(async () => {
for (let i = 0; i < 5; i++) {
const token = await solveCaptcha('6Le-SITEKEY', `https://example.com/page/${i}`);
if (token) console.log(`Solved: ${token.substring(0, 40)}...`);
}
console.log(`DLQ size: ${dlq.size()}`);
})();
Analiza los patrones de fallo de la DLQ
La DLQ no es solo un buzón de reintentos: es tu mejor fuente de diagnóstico. Cuando la revisas con regularidad, deja de ser una lista de errores y se convierte en un mapa de dónde falla tu pipeline.
Qué te dice la distribución de errores
# Export DLQ for analysis
dlq.export_json("failed-tasks.json")
# Analyze error distribution
from collections import Counter
errors = Counter(t["error"] for t in dlq.peek_all())
for error, count in errors.most_common():
print(f" {error}: {count}")
- Sitekeys que fallan una y otra vez → revisa que los parámetros sean correctos.
- Timeouts concentrados en franjas horarias concretas → correlaciónalos con la carga de la API.
- Errores de red recurrentes → comprueba el estado del proxy.
Problemas frecuentes y cómo resolverlos
Estos son los tropiezos más comunes al llevar una DLQ a producción:
| Problema | Causa | Solución |
|---|---|---|
| La DLQ crece sin control | No se procesan los reintentos | Programa un drenaje periódico con retry_dlq() |
| La misma tarea se reintenta para siempre | Falta un tope de intentos | Comprueba task.attempts antes de volver a encolar |
| El archivo de la DLQ se corrompe | Escrituras concurrentes | Usa bloqueo de archivos o migra a Redis o base de datos |
| Se pierden tareas al reiniciar | DLQ solo en memoria | Usa una DLQ en archivo o respaldada por Redis |
Preguntas frecuentes
¿Una DLQ sustituye a la lógica de reintentos con backoff?
No. Son capas complementarias. El backoff reintenta al instante los fallos transitorios; la DLQ recoge lo que sigue fallando tras agotar esos reintentos, para que puedas reprocesarlo o analizarlo sin bloquear el lote en curso.
¿Qué errores conviene reintentar y cuáles no?
Reintenta los transitorios: ERROR_NO_SLOT_AVAILABLE, los timeouts y los cortes de red suelen resolverse solos en una segunda pasada. En cambio, un ERROR_CAPTCHA_UNSOLVABLE que se repite siempre con la misma sitekey rara vez mejora reintentando; regístralo y revisa los parámetros.
¿Una tarea en la DLQ consume threads de mi plan de CaptchaAI?
No. CaptchaAI factura por thread concurrente, y un thread solo se ocupa mientras un CAPTCHA está en curso. Una tarea que espera en la DLQ no está en vuelo, así que no consume capacidad; solo la usará cuando la vuelvas a enviar con retry_dlq().
¿Puedo combinar la DLQ con el patrón circuit breaker?
Sí, y encajan muy bien. El circuit breaker corta el envío de solicitudes durante una interrupción, y la DLQ captura las tareas que fallan justo antes de que se dispare el circuito. Tienes el detalle en la guía del patrón circuit breaker.
Sigue aprendiendo
- Patrón circuit breaker para llamadas a la API de CAPTCHA
- Cómo implementar lógica de reintentos en la API de CaptchaAI
- Cola Redis + CaptchaAI para procesamiento distribuido