Um pipeline de automação que resolve milhares de CAPTCHAs por dia sempre vai ter uma fração de tarefas que falha, mesmo com nova tentativa automática. A pergunta não é se algumas tarefas vão falhar, é o que acontece com elas depois.
Sem uma fila de mensagens mortas (dead-letter queue, DLQ), a resposta padrão é a pior possível: uma linha perdida no log, sem contexto para investigar e sem forma de reprocessar depois.
Este guia mostra como montar uma DLQ para tarefas de CAPTCHA com falha — em memória, com persistência em arquivo e com reprocessamento automático — usando a API da CaptchaAI como exemplo.
Por que tarefas de CAPTCHA acabam no DLQ
Em qualquer pipeline com volume real, quatro causas respondem pela maioria das falhas:
ERROR_CAPTCHA_UNSOLVABLE— o solucionador não conseguiu concluir o desafioERROR_NO_SLOT_AVAILABLE— todos os workers ocupados e as tentativas automáticas esgotadas- Tempo limite (timeout) — o solver não devolveu um resultado dentro do prazo configurado
- Erros de rede — a conexão caiu durante o polling do resultado
Sem uma DLQ, cada uma dessas falhas vira só uma linha de log — e log, na prática, quase ninguém lê em tempo real. Times de QA que rodam suítes à noite contra ambientes de staging (por exemplo, com workers na região sa-east-1 da AWS, em São Paulo, para reduzir a latência) só costumam descobrir o problema no relatório da manhã seguinte, quando já é tarde para reprocessar com o contexto original.
Capturar a tarefa no momento da falha — sitekey, URL da página e erro — é o que torna o reprocessamento possível.
Monte uma DLQ em memória com nova tentativa (Python)
O exemplo abaixo implementa uma fila em memória com deque: toda tarefa que esgota as tentativas é empurrada para o DLQ junto com o erro, o número de tentativas e o timestamp. solve_captcha() faz o envio e o polling padrão da API da CaptchaAI, com backoff exponencial entre as tentativas.
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()}")
Resultado esperado:
Solved: 03AGdBq26ZfPxL...
Solved: 03AGdBq27AbCdE...
[dlq] Added: ERROR_CAPTCHA_UNSOLVABLE (attempts: 4)
Solved: 03AGdBq28FgHiJ...
[dlq] Added: Task 71823460 timed out (attempts: 4)
DLQ size: 2
Duas das cinco tarefas foram parar no DLQ — exatamente o tipo de falha que, sem a fila, teria virado uma linha de log perdida no meio de milhares de outras.
Reenviando tarefas do DLQ
Um DLQ sem reprocessamento é só um cemitério de tarefas.
A função retry_dlq() percorre a fila, tenta resolver de novo com um teto de tentativas próprio (max_retries) e marca como falha permanente qualquer tarefa que já tenha estourado dlq.max_retries + max_retries no total — evitando o loop de reenviar para sempre uma tarefa com parâmetro errado.
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)
DLQ com persistência em arquivo (Node.js)
Uma DLQ em memória perde tudo se o processo reiniciar. Para serviços de longa duração, persista a fila em disco — ou em Redis/banco de dados — a cada push/pop. O exemplo em Node.js grava o estado em captcha-dlq.json sempre que a fila muda:
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()}`);
})();
Encontre padrões nas falhas do DLQ
Exportar o DLQ para análise transforma erros isolados em sinal:
# 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}")
Use esses dados para:
- localizar sitekeys que falham de forma consistente → provável parâmetro incorreto (sitekey ou URL da página trocados)
- cruzar horários de timeout com picos de carga na API → ajustar o valor do timeout ou distribuir melhor o volume
- identificar concentração de erros de rede → verificar a saúde do proxy e a conectividade dos workers
Boas práticas para operar uma DLQ em produção
Antes de ir para produção, vale fechar algumas decisões:
- Alerte, não apenas armazene. Um DLQ que cresce silenciosamente é tão inútil quanto não ter DLQ nenhum — configure um callback (webhook) ou um job agendado que avisa a equipe ao passar de um limite razoável.
- Separe o teto de tentativas do DLQ do teto original. Reprocessar com o mesmo
max_retriesda tentativa inicial mascara tarefas com parâmetro errado, que voltam para a fila indefinidamente. - Escolha o backend pelo volume, não por hábito. Em memória basta para scripts pontuais; a partir de centenas de tarefas por dia, ou em serviços que reiniciam com frequência, migre para Redis ou um banco de dados.
- Trate a fila como dado potencialmente sensível. Ela guarda a URL da página e a sitekey de cada tarefa com falha — considere as obrigações de retenção e minimização de dados da LGPD (ou do RGPD, em Portugal) antes de manter esse histórico por muito tempo.
Solução de problemas
Problemas comuns ao operar uma DLQ de CAPTCHA em produção — e como resolver cada um:
| Problema | Causa | Correção |
|---|---|---|
| DLQ cresce indefinidamente | Ninguém está processando as novas tentativas | Agende uma drenagem periódica da fila com retry_dlq() |
| A mesma tarefa é reenviada para sempre | Sem limite máximo de tentativas | Verifique task.attempts antes de recolocar a tarefa na fila |
| Arquivo do DLQ corrompido | Gravações simultâneas no mesmo arquivo | Use bloqueio de arquivo ou migre para Redis/banco de dados |
| Tarefas perdidas quando o processo cai | DLQ apenas em memória | Use uma versão com persistência em arquivo ou com suporte de Redis |
Perguntas frequentes
O DLQ substitui o retry automático?
Não. O retry automático dentro de solve_captcha() trata falhas passageiras — timeout pontual, erro de rede momentâneo. O DLQ entra depois, quando as tentativas automáticas já se esgotaram, guardando o que sobrou para investigação ou reprocessamento posterior.
Como alertar a equipe quando o DLQ crescer demais?
A forma mais simples é um job agendado que consulta dlq.size() a cada poucos minutos e dispara um webhook (Slack, e-mail, PagerDuty) ao passar de um limite definido. Em volumes maiores, mover o DLQ para Redis permite consultar o tamanho da fila de qualquer processo, inclusive um painel de monitoramento separado.
DLQ em memória ou com Redis: qual usar em produção?
Depende do tempo de vida do processo. Em memória funciona bem para scripts curtos, onde perder o DLQ ao reiniciar não é grave. Para serviços de longa duração — workers rodando por dias — prefira uma versão com persistência em arquivo ou Redis, para que a reinicialização não apague tarefas pendentes.
Posso combinar isso com o padrão de disjuntor (circuit breaker)?
Sim, e os dois se complementam bem. O disjuntor interrompe o envio de novas requisições durante uma interrupção da API; o DLQ captura as tarefas que já falharam antes de o circuito abrir. Consulte o padrão de disjuntor para chamadas de API CAPTCHA.
Preciso registrar dados sensíveis do usuário no DLQ?
Só o mínimo necessário para reprocessar: sitekey, URL da página e o erro. Evite guardar dados pessoais de quem preencheu o formulário original — revise a política de retenção considerando a LGPD (ou o RGPD, em Portugal) antes de manter esse histórico por muito tempo.
Não perca mais nenhuma tarefa CAPTCHA com falha
Gere sua chave de API e transforme cada falha capturada pelo DLQ em um ajuste real no pipeline. Chave disponível em captchaai.com.