Você já registrou callbacks e implementou polling, mas ainda descobre o estado de uma tarefa CAPTCHA só vasculhando logs manualmente? A saída é tratar o ciclo de vida do CAPTCHA como eventos, e deixar que cada parte da aplicação escute só o que precisa. Os cinco estados possíveis:
- enviado — tarefa submetida ao endpoint
in.php; - pendente — polling aguardando resultado em
res.php; - resolvido — token pronto para uso;
- falhou — a API retornou erro ou a submissão não saiu;
- expirado — o
maxWaitfoi atingido sem resposta.
Este tutorial monta esse barramento em Node.js com o EventEmitter nativo — sem Kafka, sem RabbitMQ — e traz o equivalente em Python para quem roda workers fora do ecossistema JavaScript.
Como o barramento organiza o ciclo de vida do CAPTCHA
Cada chamada à API da CaptchaAI passa por até cinco estados possíveis, e cada estado vira um evento que qualquer ouvinte pode assinar de forma independente:
[CaptchaBus]
├── emit("submitted", { taskId, type, pageurl })
├── emit("pending", { taskId, elapsed })
├── emit("solved", { taskId, solution, duration })
├── emit("failed", { taskId, error, duration })
└── emit("timeout", { taskId, elapsed })
↓ ↓ ↓
[Logger] [Metrics] [Retry Handler]
Cada ouvinte cuida de uma responsabilidade só, sem saber da existência dos outros:
Loggergrava cada transição de estado no console, útil para depurar;Metricsacumula contadores agregados (submitted,solved,failed);Retry Handlerdecide, isoladamente, se vale reenviar uma tarefa que falhou.
Adicionar uma nova função — latência por região, alerta quando failed passa de um limite — não exige tocar na lógica de resolução: basta registrar mais um ouvinte.
Implementando a classe CaptchaBus em Node.js
A classe abaixo estende o EventEmitter nativo do Node.js. Ela envia a tarefa para in.php, faz o polling em res.php e emite o evento correspondente em cada etapa — sem bloquear a aplicação, já que o polling roda com setTimeout em vez de um laço síncrono:
const EventEmitter = require("events");
const axios = require("axios");
class CaptchaBus extends EventEmitter {
constructor(apiKey, options = {}) {
super();
this.apiKey = apiKey;
this.pollInterval = options.pollInterval || 5000;
this.maxWait = options.maxWait || 300000; // 5 minutes
this.pending = new Map();
}
async submit(params) {
const { method, sitekey, pageurl, ...extra } = params;
const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const submitParams = {
key: this.apiKey,
method: method || "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: 1,
...extra,
};
try {
const resp = await axios.post(
"https://ocr.captchaai.com/in.php",
null,
{ params: submitParams }
);
if (resp.data.status !== 1) {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: 0,
});
return null;
}
const captchaId = resp.data.request;
const startTime = Date.now();
this.emit("submitted", {
taskId,
captchaId,
method: method || "userrecaptcha",
pageurl,
});
// Start polling
this._poll(taskId, captchaId, startTime);
return taskId;
} catch (err) {
this.emit("failed", { taskId, error: err.message, duration: 0 });
return null;
}
}
async _poll(taskId, captchaId, startTime) {
const check = async () => {
const elapsed = Date.now() - startTime;
if (elapsed > this.maxWait) {
this.emit("timeout", { taskId, elapsed });
return;
}
this.emit("pending", { taskId, elapsed });
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: {
key: this.apiKey,
action: "get",
id: captchaId,
json: 1,
},
});
if (resp.data.status === 1) {
this.emit("solved", {
taskId,
captchaId,
solution: resp.data.request,
duration: Date.now() - startTime,
});
} else if (resp.data.request === "CAPCHA_NOT_READY") {
setTimeout(check, this.pollInterval);
} else {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: Date.now() - startTime,
});
}
} catch (err) {
this.emit("failed", {
taskId,
error: err.message,
duration: Date.now() - startTime,
});
}
};
setTimeout(check, this.pollInterval);
}
}
module.exports = CaptchaBus;
Dois detalhes fazem diferença em produção:
- o
taskIdé gerado localmente, antes da resposta da API — dá para rastrear a tarefa mesmo se a requisição falhar antes de chegar à CaptchaAI; - o
maxWait(5 minutos por padrão) evita que uma tarefa travada fique consultandores.phppara sempre: ela emitetimeoute libera o ouvinte.
Registrando os ouvintes de eventos
Com a classe pronta, registre quantos ouvintes fizerem sentido para o seu caso. O exemplo cobre os três usos mais comuns: log estruturado no console, métricas agregadas em memória e o envio da primeira tarefa:
const CaptchaBus = require("./captcha-bus");
const bus = new CaptchaBus(process.env.CAPTCHAAI_API_KEY, {
pollInterval: 5000,
maxWait: 120000,
});
// Logging listener
bus.on("submitted", (e) => {
console.log(`[SUBMIT] ${e.taskId} → ${e.method} on ${e.pageurl}`);
});
bus.on("pending", (e) => {
console.log(`[PENDING] ${e.taskId} — ${(e.elapsed / 1000).toFixed(1)}s`);
});
bus.on("solved", (e) => {
console.log(
`[SOLVED] ${e.taskId} in ${(e.duration / 1000).toFixed(1)}s — ${e.solution.substring(0, 30)}...`
);
});
bus.on("failed", (e) => {
console.error(`[FAILED] ${e.taskId} — ${e.error}`);
});
bus.on("timeout", (e) => {
console.error(
`[TIMEOUT] ${e.taskId} after ${(e.elapsed / 1000).toFixed(1)}s`
);
});
// Metrics listener
const metrics = { submitted: 0, solved: 0, failed: 0, totalDuration: 0 };
bus.on("submitted", () => metrics.submitted++);
bus.on("solved", (e) => {
metrics.solved++;
metrics.totalDuration += e.duration;
});
bus.on("failed", () => metrics.failed++);
// Submit a CAPTCHA
bus.submit({
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
Latência regional e auditoria de eventos
Se os workers rodam em uma região como sa-east-1 (São Paulo) na AWS, acrescente a latência de rede ao objeto metrics — a diferença entre a duração de solved e o tempo de resposta bruto da API separa "a CaptchaAI demorou" de "a rede até a CaptchaAI demorou". Ao persistir eventos em arquivo ou banco para auditoria, trate pageurl e qualquer dado de usuário final com as mesmas cautelas de retenção e acesso que a LGPD exige para dados coletados em produção.
Versão em Python: mesma arquitetura, outro runtime
Se o seu stack de automação roda em Python — comum em pipelines de scraping e QA —, o barramento segue o mesmo modelo de publish/subscribe, só que sobre um dicionário de callbacks em vez do EventEmitter. A lógica de submissão, polling e emissão de eventos é idêntica; muda só a sintaxe:
import os
import time
import threading
from collections import defaultdict
import requests
class CaptchaBus:
def __init__(self, api_key, poll_interval=5, max_wait=300):
self.api_key = api_key
self.poll_interval = poll_interval
self.max_wait = max_wait
self._listeners = defaultdict(list)
def on(self, event, callback):
"""Register a listener for an event."""
self._listeners[event].append(callback)
return self
def emit(self, event, data):
"""Emit an event to all registered listeners."""
for callback in self._listeners.get(event, []):
try:
callback(data)
except Exception as e:
print(f"Listener error on {event}: {e}")
def submit(self, sitekey, pageurl, method="userrecaptcha", **extra):
"""Submit a CAPTCHA and begin tracking."""
task_id = f"task_{int(time.time())}_{id(sitekey) % 10000}"
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": method,
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
**extra
})
data = resp.json()
if data.get("status") != 1:
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": 0
})
return None
captcha_id = data["request"]
start_time = time.time()
self.emit("submitted", {
"task_id": task_id,
"captcha_id": captcha_id,
"method": method,
"pageurl": pageurl
})
# Poll in a background thread
thread = threading.Thread(
target=self._poll,
args=(task_id, captcha_id, start_time),
daemon=True
)
thread.start()
return task_id
def _poll(self, task_id, captcha_id, start_time):
while True:
elapsed = time.time() - start_time
if elapsed > self.max_wait:
self.emit("timeout", {"task_id": task_id, "elapsed": elapsed})
return
time.sleep(self.poll_interval)
self.emit("pending", {"task_id": task_id, "elapsed": elapsed})
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": captcha_id,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
self.emit("solved", {
"task_id": task_id,
"solution": data["request"],
"duration": time.time() - start_time
})
return
elif data.get("request") != "CAPCHA_NOT_READY":
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": time.time() - start_time
})
return
# Usage
bus = CaptchaBus(os.environ["CAPTCHAAI_API_KEY"])
bus.on("submitted", lambda e: print(f"[SUBMIT] {e['task_id']}"))
bus.on("solved", lambda e: print(f"[SOLVED] {e['task_id']} in {e['duration']:.1f}s"))
bus.on("failed", lambda e: print(f"[FAILED] {e['task_id']} — {e['error']}"))
bus.on("timeout", lambda e: print(f"[TIMEOUT] {e['task_id']}"))
bus.submit("6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "https://example.com")
A principal diferença de implementação é que o polling roda em uma threading.Thread separada (com daemon=True, para não travar o encerramento do processo), enquanto na versão Node.js o setTimeout já é assíncrono por natureza da linguagem.
Retentativa automática como um ouvinte de eventos
Um dos maiores ganhos do modelo de eventos aparece aqui: a retentativa não precisa morar dentro da função submit(). Basta escutar o evento failed e decidir, de fora, se vale reenviar a tarefa:
// Automatic retry on failure
bus.on("failed", async (e) => {
if (e.retryCount >= 3) {
console.error(`[GIVE UP] ${e.taskId} after 3 retries`);
return;
}
console.log(`[RETRY] ${e.taskId} — attempt ${(e.retryCount || 0) + 1}`);
await bus.submit({
...e.originalParams,
_retryCount: (e.retryCount || 0) + 1,
});
});
Simplificando com um wrapper de Promise
Se o restante do seu código já usa async/await, expor o barramento como uma Promise evita que você precise escutar eventos manualmente toda vez que resolver um CAPTCHA:
function solveCaptcha(bus, params) {
return new Promise((resolve, reject) => {
const taskId = bus.submit(params);
function onSolved(e) {
if (e.taskId === taskId) {
cleanup();
resolve(e.solution);
}
}
function onFailed(e) {
if (e.taskId === taskId) {
cleanup();
reject(new Error(e.error));
}
}
function cleanup() {
bus.removeListener("solved", onSolved);
bus.removeListener("failed", onFailed);
bus.removeListener("timeout", onFailed);
}
bus.on("solved", onSolved);
bus.on("failed", onFailed);
bus.on("timeout", onFailed);
});
}
// Usage
const solution = await solveCaptcha(bus, {
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
Problemas comuns e como resolver
A tabela reúne os problemas mais comuns ao colocar esse padrão em produção — a maioria aparece na primeira semana de uso, quando o volume de tarefas simultâneas ainda está sendo ajustado ao plano de threads contratado.
| Problema | Causa | Correção |
|---|---|---|
| Ouvinte não dispara | Nome do evento não bate (ex.: "solve" em vez de "solved") | Confira os nomes exatos usados em emit/on |
| Aviso de vazamento de memória | Muitos ouvintes registrados no mesmo evento | Use setMaxListeners() ou remova ouvintes após o uso |
| Console inundado de eventos "pending" | Intervalo de polling curto demais | Aumente pollInterval para 5000 ms ou mais |
| Eventos perdidos numa retentativa | Um novo taskId é gerado a cada retry |
Repasse os parâmetros originais para reconectar o estado |
Se o problema for volume — muitas tarefas em pending ao mesmo tempo —, o gargalo pode estar no plano de threads, não no barramento: o BASIC (US$ 15/mês, 5 threads) processa 5 CAPTCHAs simultâneos; para escalar sem formar fila, migre para ADVANCE (US$ 90/mês, 50 threads) ou um plano superior.
Perguntas frequentes
Quantas threads da CaptchaAI eu preciso para essa arquitetura?
Depende de quanto o barramento processa em paralelo. Cada tarefa em pending ocupa uma thread até virar solved, failed ou timeout. Até 5 CAPTCHAs simultâneos, o BASIC (US$ 15/mês, 5 threads) basta; picos maiores pedem STANDARD (US$ 30/mês, 15 threads) ou ADVANCE (US$ 90/mês, 50 threads).
Preciso de Kafka ou RabbitMQ, ou o EventEmitter nativo já resolve?
Para uma aplicação de processo único, o EventEmitter nativo é suficiente e mais rápido de implementar. Considere Kafka, RabbitMQ ou Redis só quando múltiplos processos precisarem reagir aos mesmos eventos.
O que acontece se dois eventos "solved" chegarem para a mesma tarefa?
Na implementação deste tutorial isso não deveria ocorrer — o polling para assim que recebe status === 1. Se você adaptar a lógica, guarde os taskIds já resolvidos em um Set e ignore duplicados, por segurança.
Como testo o fluxo de eventos sem gastar créditos de verdade na API?
Substitua as chamadas HTTP por mocks (axios ou requests). Como o barramento é só um EventEmitter, dá para chamar bus.emit("solved", {...}) direto no teste e verificar se o ouvinte reage como esperado.
Esse padrão funciona fora do Node.js, por exemplo em um worker Python?
Sim — a versão em Python deste tutorial implementa o mesmo modelo de publish/subscribe com um dicionário de callbacks. A ideia central (emitir estado, ouvintes desacoplados) independe da linguagem; só a sintaxe muda.
Artigos relacionados
- Como construir pipelines de CAPTCHA para clientes com a CaptchaAI
- Benchmark de tempos de resolução de CAPTCHA na CaptchaAI
- Automação responsável com a CaptchaAI
Próximas etapas
Comece a construir seu barramento de eventos hoje: obtenha sua chave de API da CaptchaAI e conecte o primeiro ouvinte ao seu pipeline de resolução.
Guias relacionados:
- Guia de URL de callback e webhook
- SSE para notificações de CAPTCHA em tempo real
- Padrões de tratamento de erros em callbacks