Pour traiter un lot de CAPTCHA en parallèle en Node.js, utilisez Promise.allSettled : elle attend toutes les promesses et vous rend, pour chacune, soit un token, soit une erreur.
Concrètement, sur un lot de 200 tâches dont 13 partent en timeout, vous repartez avec les 187 tokens résolus. Avec Promise.all, ils seraient tous perdus.
Dans un lot, les causes d'échec sont hétérogènes : sitekey mal copié, proxy qui coupe, file d'attente saturée. Le code ci-dessous part d'un lot minimal, puis ajoute les trois couches qui comptent en production : plafond de concurrence, retry sélectif, tri des erreurs.
Promise.all ou Promise.allSettled : ce que vous perdez au premier échec
Même tableau de promesses en entrée ; toute la différence tient au traitement du premier rejet.
// 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éthode | Au premier échec | Ce que vous récupérez | Usage pertinent |
|---|---|---|---|
Promise.all |
Rejette aussitôt | Une exception, rien d'autre | Traitements tout-ou-rien |
Promise.allSettled |
Poursuit | Un status par tâche, succès comme échec |
Lots de CAPTCHA, appels réseau en masse |
Retenez la deuxième ligne : avec Promise.all, les tâches déjà envoyées continuent de consommer vos threads, mais leurs tokens sont perdus. Vous payez la résolution sans jamais en voir le résultat.
Le socle : soumettre, interroger, agréger un lot de CAPTCHA
solveCaptcha fait le travail unitaire — un POST sur in.php, puis l'interrogation régulière de res.php jusqu'au token ou au timeout.
batchSolve l'enveloppe dans Promise.allSettled et sépare les résultats en deux tableaux. L'exemple cible reCAPTCHA v2 (method=userrecaptcha) ; le squelette vaut aussi pour Cloudflare Turnstile ou GeeTest v3.
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}`);
}
})();
L'index i relie chaque rejet à sa tâche d'origine — une information que Promise.allSettled ne transporte pas.
Aligner la concurrence sur les threads de votre plan
L’envoi simultané de 1 000 CAPTCHA ne les résout pas plus vite : vous saturez le pool de sockets de Node.js et dépassez la capacité de votre plan.
La facturation CaptchaAI repose sur les threads — un thread = un CAPTCHA en vol, résolutions illimitées par thread :
- BASIC ($15/mois, 5 threads)
- STANDARD ($30/mois, 15 threads)
- ADVANCE ($90/mois, 50 threads)
- PREMIUM ($170/mois, 100 threads)
Le bon plafond de concurrence, c'est votre nombre de threads, pas la taille du lot.
Le pool ci-dessous consomme les tâches à concurrence fixe et écrit chaque résultat à son index d'origine :
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);
Relancer uniquement les erreurs transitoires
Toutes les erreurs ne méritent pas une nouvelle tentative. Le tri se fait sur le message de l'API :
- Transitoires —
TIMEOUTisolé,ERROR_NO_SLOT_AVAILABLE,ERROR_TOO_MUCH_REQUESTS: la tâche repassera au tour suivant. - Définitives —
ERROR_ZERO_BALANCE, clé du site erronée,pageurlinvalide : elles se reproduiront et gaspillent vos threads.
La boucle ci-dessous ne relance donc que la première catégorie, sur deux tentatives par défaut :
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 };
}
Suivre l'avancement d'un lot long
Un lot de plusieurs milliers de tâches tourne de longues minutes sans rien afficher : impossible de savoir s'il avance.
Incrémentez des compteurs dans le .then() et le .catch() de chaque promesse, puis réécrivez la ligne d'avancement sur la sortie standard. Dans un job planifié, remplacez process.stdout.write par un log structuré :
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;
}
Classer les résultats : résolus, transitoires, définitifs
Un tableau brut de settled n'aide personne à 3 h du matin. Découpez-le en trois catégories qui correspondent à trois décisions :
- les tokens à consommer tout de suite ;
- les tâches à remettre en file d'attente ;
- les échecs définitifs à remonter dans votre supervision.
Ce découpage alimente aussi vos métriques — taux de réussite par type, temps de résolution médian, volume de retries :
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;
}
Cas concret : le lot nocturne d'une équipe de veille tarifaire
Un job planifié à 2 h du matin sur une instance OVHcloud ou Scaleway doit relever les prix publics de 4 000 fiches produits avant 5 h. Chaque page protégée déclenche un défi CAPTCHA.
Avec un plan ADVANCE ($90/mois, 50 threads), la concurrence se fixe à 50. Le lot part en tranches de 500 tâches via batchSolveWithRetry, et le rapport final sépare échecs transitoires et définitifs.
Les chiffres ci-dessous sont des plafonds annoncés, pas des moyennes. Les résultats varient selon l'environnement, le volume et le moment de la journée.
Sur reCAPTCHA v2, la résolution est annoncée en moins de 60 secondes avec un taux de réussite élevé ; Cloudflare Turnstile passe sous les 10 secondes. Calez votre fenêtre nocturne sur ces plafonds.
Côté RGPD, limitez-vous aux données publiques et gardez les logs de lot exempts de données personnelles.
Dépannage
| Problème observé | Cause probable | Correctif |
|---|---|---|
Tout le lot part en TIMEOUT |
Concurrence supérieure aux threads du plan | Ramener le plafond au nombre de threads |
ERR_SOCKET_EXHAUSTION, ECONNRESET |
Trop de connexions HTTP ouvertes | Un http.Agent avec keepAlive et maxSockets |
| Résultats décalés | L'ordre d'achèvement n'est pas celui de soumission | Écrire chaque résultat à son index |
| Mémoire qui grimpe | Toutes les promesses restent référencées | Découper en tranches de 100 à 500 |
ERROR_ZERO_BALANCE en cours de lot |
Solde épuisé pendant l'exécution | Vérifier le solde avant, ne pas relancer |
FAQ
Combien de résolutions simultanées mon plan autorise-t-il ?
Exactement le nombre de threads de votre plan : 5 avec BASIC, 15 avec STANDARD, 50 avec ADVANCE, 100 avec PREMIUM. Fixez concurrency sur cette valeur ; au-delà, les tâches attendent un slot libre sans rien accélérer.
Pourquoi mon lot ne se termine-t-il jamais ?
Parce que Promise.allSettled ne rend la main qu'à la dernière tâche réglée, et qu'une promesse sans borne de temps attend indéfiniment. Chaque appel unitaire doit porter son timeout : la boucle d'interrogation sort après 60 tours et lève TIMEOUT.
CaptchaAI prend-il en charge hCaptcha dans un traitement par lots ?
Non — hCaptcha et FunCaptcha (Arkose Labs) ne sont pas pris en charge, et GeeTest v4 reste annoncé comme à venir. Ce que le lot couvre : reCAPTCHA v2 et v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, les CAPTCHA image/OCR et les grilles d'images. CaptchaFox (bêta), Friendly Captcha (bêta) et Lemin (bêta) sont disponibles au stade bêta uniquement.
Une librairie comme p-limit vaut-elle mieux qu'un pool maison ?
Les deux tiennent la charge. Le pool montré plus haut évite une dépendance ; p-limit ou p-queue deviennent utiles dès que vous ajoutez des priorités, une file persistante ou un backoff exponentiel partagé.
Faut-il garder les tokens résolus pour les réutiliser ?
Non. Un token de CAPTCHA a une durée de vie courte et doit partir vers le formulaire cible dans la foulée. Résolvez à la demande, traitez l'expiration comme une erreur transitoire.
Pour aller plus loin
Un lot robuste tient en quatre décisions : Promise.allSettled, une concurrence calée sur vos threads, un retry limité aux erreurs transitoires, un tri des résultats lisible par votre supervision. Récupérez votre clé API CaptchaAI et validez le script sur un petit lot.
Guides associés :