Ein Batch-Lauf über 200 CAPTCHA-Aufgaben ist erst brauchbar, wenn er am Ende 197 Tokens und drei benannte Fehler liefert – statt bei Aufgabe 14 auszusteigen. Genau dafür ist Promise.allSettled gebaut: Es wartet, bis jedes Promise entschieden ist, und liefert pro Aufgabe einen Status. Kein Teilergebnis geht verloren, kein Fehlschlag reißt die übrigen Lösungen mit.
Dieser Leitfaden geht den Weg vom ersten axios-Request bis zum ausgewerteten Batch-Report: Solve-Zyklus, Parallelitätslimit, Wiederholungslogik, Fortschrittsausgabe und Ergebniskategorien. Alle Beispiele sprechen die CaptchaAI-Endpunkte in.php und res.php an.
Das Wesentliche vorweg
Promise.allbricht beim ersten Fehler ab;Promise.allSettledliefert für jede Aufgabe entwederfulfilledoderrejected.- Koppeln Sie den Parallelitätsgrad an Ihr Thread-Kontingent – nicht an die Länge der Aufgabenliste.
- Transiente Fehler wie
TIMEOUToderERROR_NO_SLOT_AVAILABLEwerden wiederholt, permanente Fehler nicht. - Tokens sind kurzlebig – bei reCAPTCHA rund 120 Sekunden. Lösen Sie sie direkt vor der Übermittlung an das Zielformular.
Was Promise.all in einem Batch-Lauf kostet
Promise.all ist eine Alles-oder-nichts-Zusage: Sobald ein Promise ablehnt, verwirft die Kombination das Gesamtergebnis. Die übrigen Anfragen laufen zwar weiter, ihre Tokens holt aber niemand mehr ab. Bei fünfzig gleichzeitigen CAPTCHA-Abfragen genügt eine Zeitüberschreitung, um den ganzen Lauf wertlos zu machen.
// 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}]
| Methode | Beim ersten Fehlschlag | Rückgabewert | Geeignet für |
|---|---|---|---|
Promise.all |
bricht sofort ab | nichts (wirft eine Exception) | Alles-oder-nichts-Operationen |
Promise.allSettled |
läuft weiter | jedes Einzelergebnis mit Status | Batch-CAPTCHA-Läufe |
Der Solve-Zyklus: übermitteln, Status abfragen, Token einsetzen
Jede Lösung folgt demselben Ablauf. Sie übermitteln die Aufgabe per POST an in.php – bei reCAPTCHA v2 mit method=userrecaptcha, dem googlekey und der pageurl – und erhalten eine Task-ID zurück. Danach fragen Sie den Status alle fünf Sekunden über res.php ab, bis die Antwort nicht mehr CAPCHA_NOT_READY lautet. Alles andere ist das fertige Token oder ein Fehlercode, den Sie nicht stillschweigend verwerfen sollten.
Kalkulieren Sie das Zeitfenster großzügig: Laut SLA-Obergrenzen liegt Cloudflare Turnstile bei unter 10 Sekunden, GeeTest v3 bei unter 12 Sekunden und reCAPTCHA v2 bei unter 60 Sekunden. Die Schleife unten deckt mit 60 Durchläufen à 5 Sekunden auch den langsamsten Fall ab.
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}`);
}
})();
Parallelität an das Thread-Budget koppeln
1.000 Aufgaben gleichzeitig loszuschicken, überlastet vor allem Ihre eigenen HTTP-Verbindungen. Ein Limiter mit fester Worker-Zahl hält die offenen Anfragen konstant und arbeitet die Warteschlange geordnet ab.
Die zweite Grenze steckt in Ihrem Tarif: CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung, und jede Stufe enthält unbegrenzte Lösungen je Thread. BASIC (15 $/Monat, 5 Threads) trägt kleine Testläufe, ADVANCE (90 $/Monat, 50 Threads) deckt typische Crawl-Fenster ab, PREMIUM (170 $/Monat, 100 Threads) ist für dauerhaft parallele Pipelines gedacht. Setzen Sie concurrency nie höher als Ihr Kontingent – sonst produzieren Sie vor allem ERROR_NO_SLOT_AVAILABLE.
Hinweis: Alle Preise in US-Dollar; maßgeblich ist die aktuelle Preisseite von CaptchaAI.
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);
Transiente Fehler gezielt wiederholen
Nicht jeder Fehlschlag verdient einen zweiten Versuch. TIMEOUT, ERROR_NO_SLOT_AVAILABLE und ERROR_TOO_MUCH_REQUESTS sind Last- und Zeitprobleme – dieselbe Aufgabe kann im nächsten Durchlauf glatt durchgehen. Ein falscher sitekey, eine unerreichbare pageurl oder ein leeres Guthaben bleiben dagegen auch beim zweiten Mal falsch. Filtern Sie deshalb vor jeder Wiederholung und begrenzen Sie die Zahl der Versuche.
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 };
}
Fortschritt sichtbar machen – auch im CI-Log
Bei Läufen über mehrere Minuten will jemand wissen, ob noch etwas passiert. Die folgende Variante zählt in den Promise-Ketten mit und schreibt den Stand in eine Statuszeile. Ein Hinweis für GitLab CI und GitHub Actions: Das Carriage Return \r überschreibt im Job-Log nichts, sondern erzeugt Tausende Zeilen – loggen Sie dort besser nach je 25 erledigten Aufgaben eine reguläre Zeile.
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;
}
Ergebnisse in verwertbare Kategorien einsortieren
Ein Array aus fulfilled und rejected ist noch kein Ergebnis, sondern Rohmaterial. Die Auswertung trennt drei Gruppen:
- gelöste Aufgaben samt Token,
- transiente Fehler für die nächste Runde,
- permanente Fehler, die in ein Ticket gehören.
Wichtig ist der Indexbezug – Promise.allSettled behält die Reihenfolge der übergebenen Promises bei, egal welche Aufgabe zuerst fertig wird.
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;
}
Praxisbeispiel: nächtlicher Regressionslauf in der eigenen Staging-Umgebung
Ein Softwareanbieter aus München prüft jede Nacht 240 Login- und Formularstrecken seiner eigenen Staging-Umgebung unter https://staging.example-app.test. Der Job läuft als GitLab-CI-Pipeline auf einem Hetzner-Server, jede Strecke enthält ein reCAPTCHA v2. Bei einem Parallelitätsgrad von 40 laufen die 240 Aufgaben in sechs Wellen; die Wellendauer richtet sich nach der Lösungszeit des CAPTCHA-Typs.
Der Unterschied zum früheren Promise.all-Aufbau zeigt sich, sobald eine Strecke hängt: Statt eines Abbruchs ohne Befund liegt am Morgen ein Report mit 238 grünen Strecken und zwei benannten Fehlern vor. Berührt ein Lauf produktive Systeme, gehört die datenschutzrechtliche Prüfung dazu – IP-Adressen sind nach DSGVO personenbezogene Daten.
Typische Stolperfallen im Batch-Betrieb
| Symptom | Ursache | Abhilfe |
|---|---|---|
| Alle Aufgaben laufen in ein Timeout | Parallelitätsgrad über dem Thread-Kontingent | auf 5–10 gleichzeitige Aufgaben zurückgehen |
ERR_SOCKET_EXHAUSTION |
zu viele gleichzeitige HTTP-Verbindungen | http.Agent mit begrenztem maxSockets verwenden |
| Ergebnisse passen nicht zu den Aufgaben | Abschlussreihenfolge ist nicht die Übermittlungsreihenfolge | Ergebnisse über den Index ablegen |
| Speicherverbrauch wächst bei großen Läufen | alle Promises liegen im Speicher | in Blöcken von 100–500 Aufgaben verarbeiten |
| Token wird erzeugt, aber vom Zielformular abgelehnt | sitekey, pageurl oder Sitzungskontext passen nicht zusammen |
Parameter erneut auslesen, Token in derselben Sitzung eintragen |
Häufige Fragen
Wie viele Threads brauche ich für 500 CAPTCHAs pro Nacht?
50 Threads genügen: ADVANCE (90 $/Monat, 50 Threads) arbeitet 500 Aufgaben in zehn Wellen ab. Entscheidend ist nicht die Gesamtzahl, sondern wie viele Aufgaben gleichzeitig laufen. Da jede Stufe unbegrenzte Lösungen je Thread enthält, kostet die zehnte Welle nicht mehr als die erste.
Blockiert eine einzelne hängende Aufgabe den gesamten Batch?
Nein, aber sie verzögert ihn. Promise.allSettled wird erst fertig, wenn auch das letzte Promise entschieden ist – ohne eigenes Timeout wartet der Lauf auf den langsamsten Teilnehmer. Die Zählschleife um res.php liefert genau dieses Timeout.
Was mache ich mit Tokens, die vor dem Absenden ablaufen?
Verkürzen Sie den Abstand zwischen Lösung und Verwendung. Ein reCAPTCHA-Token ist rund 120 Sekunden gültig; lösen Sie es direkt vor der Übermittlung an das Formular, nicht am Anfang eines langen Laufs. Bei großen Mengen in Blöcken arbeiten und jeden Block sofort absenden.
Funktioniert dasselbe Muster für Turnstile, GeeTest v3 und Bild-CAPTCHAs?
Ja – nur der method-Parameter und die Felder ändern sich, der Ablauf aus Übermittlung, Polling und Token bleibt identisch. Abgedeckt sind die reCAPTCHA-Familie, Cloudflare Turnstile und Cloudflare Challenge, GeeTest v3, Bild- und Rasterbild-CAPTCHAs sowie BLS; CaptchaFox, Friendly Captcha und Lemin laufen als Beta. Nicht unterstützt werden hCaptcha und FunCaptcha, und GeeTest v4 ist bislang nur als „bald verfügbar“ angekündigt.
Ab welcher Node.js-Version steht Promise.allSettled bereit?
Ab Node.js 12.9 gehört die Methode zur Standardbibliothek, ist also in allen aktuellen LTS-Versionen verfügbar. Für ältere Laufzeiten versehen Sie jedes Promise mit einem .catch(), das den Fehler als Wert zurückgibt, und rufen Promise.all auf.
Nächste Schritte
Starten Sie mit zehn parallelen Aufgaben, messen Sie Durchsatz und Fehlerquote und erhöhen Sie den Wert erst, wenn beide Kennzahlen stabil bleiben. Ein Batch-Lauf, der Teilergebnisse sauber meldet, spart mehr Zeit als jede weitere Optimierung.