Tutoriales de API

Node.js Promise.allSettled para resolver CAPTCHA por lotes

Para resolver muchos CAPTCHA en paralelo desde Node.js, la herramienta correcta es Promise.allSettled, no Promise.all. La diferencia es simple pero decisiva: Promise.all aborta el lote entero en cuanto una promesa se rechaza, mientras que Promise.allSettled espera a que todas se liquiden y te devuelve el resultado de cada una —los aciertos y los fallos— en un solo array. Cuando lanzas decenas de resoluciones simultáneas y una agota su tiempo de espera o devuelve un error, esa distinción decide si pierdes todo el lote o conservas el resto. En esta guía montamos un flujo por lotes robusto contra la API de CaptchaAI.

Promise.all frente a Promise.allSettled en la resolución por lotes

Antes de escribir el bucle de resolución conviene tener clara la diferencia entre ambos métodos, porque condiciona todo el diseño del flujo. Promise.all está pensado para operaciones de todo o nada: si cualquiera de las promesas se rechaza, el await lanza y descartas el trabajo ya completado. Promise.allSettled nunca lanza; siempre resuelve con un array donde cada entrada trae su status (fulfilled o rejected) y su value o reason correspondiente.

// 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 Ante el primer fallo Devuelve Ideal para
Promise.all Se rechaza de inmediato Nada (lanza) Operaciones de todo o nada
Promise.allSettled Continúa Cada resultado Resolución de CAPTCHA por lotes

En un lote de CAPTCHA, donde algún fallo puntual es inevitable, Promise.allSettled es casi siempre la elección acertada.

Implementación base: enviar y sondear cada CAPTCHA

El núcleo del flujo es una función que envía un CAPTCHA al endpoint in.php, sondea res.php hasta obtener la solución y lanza un error si algo sale mal. A partir de ahí, batchSolve mapea todas las tareas a promesas y las agrupa con Promise.allSettled, separando después los aciertos de los fallos con un almacenamiento basado en índices para no perder la correspondencia con la tarea original.

El flujo se apoya en dos piezas bien delimitadas:

  • solveCaptcha — envía una tarea al endpoint in.php, sondea res.php y devuelve la solución o lanza un error.
  • batchSolve — lanza todas las tareas juntas con Promise.allSettled y reparte la respuesta en dos listas: aciertos y fallos.

El lote completo de principio a fin

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}`);
  }
})();

Controlar la concurrencia sin saturar las conexiones

Enviar 1000 CAPTCHA a la vez satura las conexiones y dispara los errores. Conviene limitar cuántas resoluciones viajan en simultáneo con un pool de workers.

Alinea la concurrencia con los threads de tu plan

Un buen punto de partida es igualar la concurrencia a los threads de tu plan de CaptchaAI: con ADVANCE ($90/mes, 50 threads) fija la concurrencia en torno a 50; con BASIC ($15/mes, 5 threads), mantenla en 5. Es el patrón habitual cuando una agencia en Ciudad de México o Buenos Aires monitorea precios en marketplaces regionales y necesita resolver cientos de CAPTCHA por hora sin ahogar su salida de red.

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);

El almacenamiento por índice (results[i]) mantiene cada resultado en la misma posición que su tarea, aunque las resoluciones terminen en desorden.

Reintentar solo las tareas que fallaron

No todos los fallos merecen un reintento. Un sitekey inválido volverá a fallar siempre; en cambio, un TIMEOUT, un ERROR_NO_SLOT_AVAILABLE o un ERROR_TOO_MUCH_REQUESTS suelen ser transitorios y desaparecen al reintentar. Conviene separar dos familias de errores antes de decidir:

  • Transitorios (merecen reintento): TIMEOUT, ERROR_NO_SLOT_AVAILABLE y ERROR_TOO_MUCH_REQUESTS.
  • Permanentes (no los reintentes): un sitekey inválido, una pageurl mal formada o parámetros incorrectos.

Esta función reenvía automáticamente solo los CAPTCHA con errores recuperables, hasta un número máximo de intentos:

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 };
}

Mostrar el progreso del lote en tiempo real

En lotes grandes conviene ver el avance mientras se ejecuta, en lugar de esperar a ciegas. Envolviendo cada promesa con contadores de completadas, correctas y fallidas puedes imprimir una línea de progreso que se actualiza en vivo:

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;
}

Clasificar los resultados de Promise.allSettled

Cuando Promise.allSettled termina, conviene traducir su array crudo a categorías con las que puedas actuar: resueltas, errores transitorios y errores permanentes. Así decides de un vistazo qué reintentar y qué registrar para revisión manual:

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;
}

Con las tres categorías separadas, cada una tiene un destino claro:

  • solved — guarda el token y continúa con tu flujo.
  • transientErrors — reencólalos para un nuevo intento.
  • permanentErrors — regístralos para revisión manual.

Buenas prácticas para lotes grandes

Cuando el volumen crece, unos pocos hábitos evitan la mayoría de los problemas antes de que aparezcan:

  • Ajusta la concurrencia a los threads de tu plan y súbela solo si la tasa de error se mantiene baja.
  • Divide los lotes muy grandes en trozos de 100–500 tareas para contener la memoria.
  • Reintenta únicamente los errores transitorios y con un tope de intentos.
  • Registra cada fallo permanente junto a su pageurl para depurarlo después.

Solución de problemas en la resolución por lotes

Cuando un lote se comporta mal, casi siempre es por uno de estos motivos:

Problema Causa Solución
Todas las tareas agotan el tiempo de espera Demasiadas solicitudes simultáneas saturan CaptchaAI o el proxy Baja la concurrencia a 5-10
ERR_SOCKET_EXHAUSTION Demasiadas conexiones HTTP a la vez Usa http.Agent con un límite maxSockets
El array de resultados queda desordenado El orden de finalización asíncrona difiere del de envío Usa almacenamiento por índice (como se muestra arriba)
La memoria crece en lotes grandes Mantienes todas las promesas en memoria a la vez Procesa en trozos de 100-500

Preguntas frecuentes

¿Cuántos threads necesito para resolver CAPTCHA en paralelo?

Depende de la concurrencia que busques. Como CaptchaAI factura por thread concurrente con resoluciones ilimitadas, cada CAPTCHA en vuelo ocupa un thread: si quieres 50 resoluciones simultáneas, necesitas un plan con al menos 50 threads, como ADVANCE ($90/mes, 50 threads). Alinea la concurrencia de tu código con los threads que tengas contratados.

¿Promise.allSettled conserva el orden de las tareas?

Sí. El array que devuelve respeta el orden en que pasaste las promesas, no el orden en que terminaron. Aun así, si dentro de cada worker guardas resultados por índice (results[i]), evitas cualquier ambigüedad cuando las resoluciones acaban en desorden.

¿Cómo distingo un error temporal de uno permanente?

Trata TIMEOUT, ERROR_NO_SLOT_AVAILABLE y ERROR_TOO_MUCH_REQUESTS como transitorios: reintentar suele resolverlos. Un sitekey mal formado o unos parámetros incorrectos son permanentes, y no deberías reintentarlos porque solo gastan tiempo y threads.

¿Sirve Promise.allSettled para un flujo continuo de scraping?

No del todo. Promise.allSettled está orientado a lotes: envías todo y esperas. Para un flujo continuo —por ejemplo, extraer páginas sin parar— encaja mejor un generador asíncrono o un pool de workers alimentado por una cola.

Próximos pasos

Ya tienes el patrón completo para resolver CAPTCHA por lotes en Node.js sin perder resultados por fallos parciales. Crea tu cuenta y consigue tu clave API de CaptchaAI para lanzar tu primer lote en paralelo.

Guías relacionadas:

Los comentarios están deshabilitados para este artículo.