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 endpointin.php, sondeares.phpy devuelve la solución o lanza un error.batchSolve— lanza todas las tareas juntas conPromise.allSettledy 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_AVAILABLEyERROR_TOO_MUCH_REQUESTS. - Permanentes (no los reintentes): un
sitekeyinválido, unapageurlmal 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
pageurlpara 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: