API Tutorials

Node.js Promise.allSettled para resolução de CAPTCHA em lote

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:

Os comentários estão desativados para este artigo.