Para resolver dezenas de CAPTCHAs em paralelo no Node.js sem perder as respostas que já chegaram, use Promise.allSettled — não Promise.all. Se o CAPTCHA número 23 de um lote de 50 falhar, Promise.all rejeita a promise inteira e as outras 49 respostas somem; Promise.allSettled espera cada tarefa terminar e devolve sucessos e falhas separados, prontos para você decidir o que fazer com cada um.
Este guia monta um pipeline completo, peça por peça:
- função que resolve um CAPTCHA por vez;
- limitador de simultaneidade;
- retentativa automática para erros transitórios;
- acompanhamento de progresso;
- categorização final dos resultados.
O código usa a API HTTP in.php/res.php da CaptchaAI e serve para qualquer tipo suportado — reCAPTCHA v2/v3, Turnstile, GeeTest v3, imagem/OCR — bastando trocar o method.
Promise.all vs. Promise.allSettled no Node.js: o que muda para lotes de CAPTCHA
Promise.all foi pensada para operações em que todas as partes precisam dar certo — se uma falhar, não faz sentido continuar. Promise.allSettled é o oposto: você quer o resultado de cada tarefa, sucesso ou falha, sem que uma exceção isolada derrube o lote inteiro.
// Promise.all — REJECTS if ANY task fails
const results = await Promise.all(tasks.map(solve)); // Throws on first error
// Promise.allSettled — RESOLVES always, with status for each
const results = await Promise.allSettled(tasks.map(solve));
// [{status: "fulfilled", value: "..."}, {status: "rejected", reason: Error}]
| Método | Na primeira falha | O que retorna | Melhor para |
|---|---|---|---|
Promise.all |
Rejeita imediatamente | Nada — lança uma exceção | Operações tudo ou nada |
Promise.allSettled |
Continua até o fim | O resultado de cada tarefa | Resolução de CAPTCHA em lote |
Numa rotina de scraping ou de testes automatizados, a segunda opção quase sempre vence.
Passo 1: função base para resolver um CAPTCHA e rodar o lote
O ponto de partida é uma função solveCaptcha que envia a tarefa para in.php, faz o polling em res.php a cada 5 segundos e devolve o token — ou lança um erro com o código retornado pela CaptchaAI. A função batchSolve mapeia as tarefas para promises e usa Promise.allSettled para separar sucessos de falhas.
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function solveCaptcha(sitekey, pageurl) {
// Submit
const submitResp = await axios.post(
"https://ocr.captchaai.com/in.php",
null,
{
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: 1,
},
}
);
if (submitResp.data.status !== 1) {
throw new Error(submitResp.data.request);
}
const captchaId = submitResp.data.request;
// Poll
for (let i = 0; i < 60; i++) {
await sleep(5000);
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (result.data.status === 1) return result.data.request;
if (result.data.request !== "CAPCHA_NOT_READY") {
throw new Error(result.data.request);
}
}
throw new Error("TIMEOUT");
}
async function batchSolve(tasks) {
const promises = tasks.map((task) =>
solveCaptcha(task.sitekey, task.pageurl).then((solution) => ({
...task,
solution,
}))
);
const results = await Promise.allSettled(promises);
const solved = [];
const failed = [];
for (let i = 0; i < results.length; i++) {
if (results[i].status === "fulfilled") {
solved.push(results[i].value);
} else {
failed.push({
task: tasks[i],
error: results[i].reason.message,
});
}
}
return { solved, failed };
}
// Usage
(async () => {
const tasks = [
{
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com/page/1",
},
{
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com/page/2",
},
{
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com/page/3",
},
];
const { solved, failed } = await batchSolve(tasks);
console.log(`Solved: ${solved.length}, Failed: ${failed.length}`);
for (const s of solved) {
console.log(` ✓ ${s.pageurl}: ${s.solution.substring(0, 30)}...`);
}
for (const f of failed) {
console.log(` ✗ ${f.task.pageurl}: ${f.error}`);
}
})();
Repare que results[i] mantém a mesma ordem de tasks, mesmo que as respostas cheguem fora de ordem — é isso que permite juntar task e error corretamente na hora de montar o array failed.
Passo 2: limite a simultaneidade para não sobrecarregar a API
Disparar mil requisições de uma vez satura conexões TCP e derruba a taxa de sucesso — não porque a CaptchaAI falhe, mas porque o Node.js ou o proxy na frente dele não aguentam o volume. Um pool fixo de workers resolve isso: cada worker puxa a próxima tarefa da fila assim que termina a anterior, mantendo sempre concurrency requisições em voo.
async function batchSolveWithLimit(tasks, concurrency = 10) {
const results = [];
let index = 0;
async function worker() {
while (index < tasks.length) {
const i = index++;
const task = tasks[i];
try {
const solution = await solveCaptcha(task.sitekey, task.pageurl);
results[i] = { status: "fulfilled", value: { ...task, solution } };
} catch (err) {
results[i] = { status: "rejected", reason: err };
}
}
}
// Launch concurrent workers
const workers = Array.from({ length: concurrency }, () => worker());
await Promise.allSettled(workers);
const solved = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
const failed = results
.filter((r) => r.status === "rejected")
.map((r, i) => ({ task: tasks[i], error: r.reason.message }));
return { solved, failed };
}
// Solve 100 CAPTCHAs, 10 at a time
const { solved, failed } = await batchSolveWithLimit(tasks, 10);
Quantas threads o seu plano CaptchaAI libera
O concurrency do limitador não deveria ultrapassar as threads do seu plano — cada thread é um CAPTCHA em voo por vez, e a CaptchaAI cobra por thread simultânea, não por CAPTCHA resolvido:
- BASIC — US$ 15/mês, 5 threads
- STANDARD — US$ 30/mês, 15 threads
- ADVANCE — US$ 90/mês, 50 threads
- PREMIUM — US$ 170/mês, 100 threads
- CORPORATE — US$ 240/mês, 150 threads
- ENTERPRISE — US$ 300/mês, 200 threads
Rodar concurrency: 50 num plano BASIC de 5 threads não acelera nada — as requisições extras ficam na fila esperando uma thread livre.
Passo 3: repita automaticamente as falhas transitórias
Nem todo erro merece uma nova tentativa. Timeout de polling, ERROR_NO_SLOT_AVAILABLE e ERROR_TOO_MUCH_REQUESTS costumam ser transitórios — a CaptchaAI estava ocupada por um instante. Já uma chave de API inválida ou saldo zerado não somem com uma nova tentativa; insistir só desperdiça tempo. A função abaixo roda o lote em rodadas, filtra os erros transitórios e reenvia só essas tarefas, até um número máximo de tentativas.
async function batchSolveWithRetry(tasks, maxRetries = 2, concurrency = 10) {
let currentTasks = [...tasks];
let allSolved = [];
for (let attempt = 0; attempt <= maxRetries; attempt++) {
if (currentTasks.length === 0) break;
console.log(
`Attempt ${attempt + 1}: solving ${currentTasks.length} tasks...`
);
const { solved, failed } = await batchSolveWithLimit(
currentTasks,
concurrency
);
allSolved = [...allSolved, ...solved];
// Only retry transient errors
const retryable = failed.filter(
(f) =>
f.error === "TIMEOUT" ||
f.error === "ERROR_NO_SLOT_AVAILABLE" ||
f.error === "ERROR_TOO_MUCH_REQUESTS"
);
currentTasks = retryable.map((f) => f.task);
if (retryable.length > 0) {
console.log(` Retrying ${retryable.length} failed tasks...`);
}
}
const finalFailed = currentTasks; // Anything left after all retries
return { solved: allSolved, failed: finalFailed };
}
Passo 4: acompanhe o progresso do lote em tempo real
Em lotes grandes — centenas ou milhares de tarefas — esperar em silêncio até o Promise.allSettled final resolver é desconfortável, tanto para quem está depurando quanto para um pipeline de CI que precisa reportar status. A versão abaixo escreve o progresso na mesma linha do terminal a cada tarefa concluída, com as contagens de sucesso e erro separadas.
async function batchSolveWithProgress(tasks, concurrency = 10) {
let completed = 0;
let succeeded = 0;
let failed = 0;
const wrapped = tasks.map((task) =>
solveCaptcha(task.sitekey, task.pageurl)
.then((solution) => {
succeeded++;
completed++;
process.stdout.write(
`\rProgress: ${completed}/${tasks.length} (${succeeded} ok, ${failed} err)`
);
return { ...task, solution };
})
.catch((err) => {
failed++;
completed++;
process.stdout.write(
`\rProgress: ${completed}/${tasks.length} (${succeeded} ok, ${failed} err)`
);
throw err;
})
);
const results = await Promise.allSettled(wrapped);
console.log("\nDone.");
return results;
}
Passo 5: organize os resultados do lote de CAPTCHA em categorias acionáveis
Depois que o Promise.allSettled termina, o array bruto de resultados raramente é útil direto. O próximo passo natural é separar sucessos, erros transitórios (candidatos a retentativa) e erros permanentes (que precisam de intervenção manual, como saldo ou configuração).
function categorizeResults(settled, originalTasks) {
const categories = {
solved: [],
transientErrors: [],
permanentErrors: [],
};
const TRANSIENT = new Set([
"TIMEOUT",
"ERROR_NO_SLOT_AVAILABLE",
"ERROR_TOO_MUCH_REQUESTS",
]);
for (let i = 0; i < settled.length; i++) {
const r = settled[i];
if (r.status === "fulfilled") {
categories.solved.push(r.value);
} else {
const error = r.reason.message;
const entry = { task: originalTasks[i], error };
if (TRANSIENT.has(error)) {
categories.transientErrors.push(entry);
} else {
categories.permanentErrors.push(entry);
}
}
}
return categories;
}
Cuidado com o que fica nos logs
Os objetos em failed guardam a task original, incluindo pageurl. Se você persiste esses logs e a URL ou o payload contiver dado pessoal (CPF, e-mail, endereço de um formulário de checkout), isso vira dado pessoal sob a LGPD — defina um período de retenção e evite guardar mais do que o necessário para depurar o pipeline.
Solução de problemas mais comuns
Estes problemas aparecem em lotes grandes, raramente em testes com duas ou três tarefas.
| Problema | Causa | Correção |
|---|---|---|
| Tempo limite de todas as tarefas | Muitas requisições simultâneas sobrecarregam a CaptchaAI ou o proxy | Reduza a simultaneidade para 5–10 |
ERR_SOCKET_EXHAUSTION |
Muitas conexões HTTP simultâneas | Use http.Agent com limite de maxSockets |
| Array de resultados fora de ordem | A ordem de conclusão assíncrona difere da ordem de envio | Use armazenamento de resultado baseado em índice (como no exemplo acima) |
| Memória crescendo em lotes grandes | Todas as promises ficam em memória ao mesmo tempo | Processe em blocos de 100–500 tarefas |
Perguntas frequentes
Quantas tarefas simultâneas o Promise.allSettled aguenta sem sobrecarregar a API?
Quem limita é a rede e o seu plano CaptchaAI, não o Promise.allSettled. Comece com concurrency: 10 e ajuste pela taxa de erro.
O que acontece se o processo Node.js cair no meio do lote?
As tarefas que já retornaram token continuam válidas — só o array em memória se perde. Em lotes grandes, grave o progresso em disco (ou numa fila como Redis) a cada N tarefas, para retomar de onde parou em vez de reenviar tudo.
Meu plano CaptchaAI limita a simultaneidade que eu posso usar?
Sim — cada plano tem um número fixo de threads simultâneas (BASIC 5, STANDARD 15, e assim por diante), e isso limita seu paralelismo real, não o número de requisições por mês. Mantenha o concurrency do código dentro desse teto.
Existe diferença real entre Promise.allSettled e um try/catch dentro de um for?
Pouca, na prática — um for com try/catch por iteração também isola falhas. A vantagem do Promise.allSettled é disparar tudo de uma vez e deixar o event loop cuidar da concorrência, sem esperar cada tarefa terminar para iniciar a próxima.
Próximos passos
Comece pelo limitador de simultaneidade e pela retentativa automática — juntos cobrem a maior parte dos problemas reais. Crie sua conta e pegue sua chave de API da CaptchaAI e teste com o seu volume de tarefas.
Guias relacionados: