Если из 500 задач на решение CAPTCHA хотя бы одна обрывается по таймауту, Promise.all уронит весь пакет — и вы потеряете 499 уже готовых результатов вместе с ней. Promise.allSettled устроен иначе: он дожидается каждого промиса и возвращает статус по каждой задаче отдельно, fulfilled или rejected, без единого «эффекта домино». Для пакетного решения CAPTCHA через API CaptchaAI это ровно то поведение, которое нужно.
Из этого руководства вы получите рабочий код для четырёх задач:
- отправка и опрос CaptchaAI без потери результатов при частичном сбое;
- ограничение параллелизма под лимит потоков тарифа;
- повтор только временных ошибок, а не всех подряд;
- разбор результатов на «решено» / «повторить» / «окончательный отказ».
Чем Promise.allSettled отличается от Promise.all
Разница видна уже в сигнатуре:
// 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}]
| Метод | При первой ошибке | Что возвращает | Когда использовать |
|---|---|---|---|
Promise.all |
Останавливается сразу | Ничего — бросает исключение | Задачи «всё или ничего» |
Promise.allSettled |
Продолжает выполнение | Результат по каждой задаче | Пакетное решение CAPTCHA |
Для пакетной обработки годится только правая строка таблицы — левая теряет данные при первом же сбое.
Базовая реализация: отправка и опрос CaptchaAI
Функция ниже отправляет reCAPTCHA v2 в in.php, опрашивает res.php до готовности результата и оборачивает вызов в Promise.allSettled, чтобы падение одной задачи не останавливало остальные:
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}`);
}
})();
Пример из практики. Инженер QA-автоматизации прогоняет ночной регресс-набор из 200+ форм регистрации на стейджинг-окружениях, часть которых защищена reCAPTCHA v2, часть — Cloudflare Turnstile. Если одна форма недоступна или отдаёт ошибку, это не должно останавливать проверку оставшихся 199 — именно за этим здесь
Promise.allSettled, а неPromise.all.
Ограничение параллелизма запросов
Отправить 1000 CAPTCHA одновременно — верный способ упереться в лимит соединений и в лимит потоков тарифа. Ограничитель параллелизма ниже держит фиксированное число «воркеров», которые разбирают очередь задач по одной:
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);
Значение concurrency не должно превышать число потоков вашего тарифа, иначе лишние запросы получают ERROR_NO_SLOT_AVAILABLE вместо результата, а не решаются быстрее:
- BASIC — 5 потоков ($15/мес);
- STANDARD — 15 потоков ($30/мес);
- ADVANCE — 50 потоков ($90/мес).
Если сервис развёрнут в европейском регионе или в Казахстане и обмен с API CaptchaAI идёт с заметной задержкой, держите тайм-аут опроса консервативным — иначе таймауты будут срабатывать чаще реальных сбоев.
Повторная отправка задач при временных ошибках
Не каждая ошибка достойна повтора: таймаут и нехватку слотов имеет смысл переотправить, а синтаксическую ошибку в sitekey — нет. Функция ниже повторяет только временные коды ошибок и не более maxRetries раз:
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 };
}
Прогресс выполнения в реальном времени
На пакетах в сотни задач полезно видеть, сколько уже решено, ещё до завершения всего Promise.allSettled — особенно если процесс запущен в фоне и логи уходят в общий дашборд мониторинга, а не только в консоль разработчика. Обёртка ниже логирует прогресс по ходу выполнения:
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;
}
Разбор результатов по категориям
После Promise.allSettled результаты стоит сразу разложить по трём категориям, чтобы дальше по пайплайну не парсить reason.message вручную:
- решено — есть готовый токен;
- временная ошибка —
TIMEOUT,ERROR_NO_SLOT_AVAILABLE,ERROR_TOO_MUCH_REQUESTS— кандидат на повтор; - окончательная ошибка — неверный sitekey или другая логическая проблема — повторять бессмысленно.
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;
}
Типичные проблемы при пакетном решении
Переход с одиночных запросов на пакетную обработку почти всегда вскрывает одни и те же узкие места — обычно они связаны не с логикой Promise.allSettled, а с сетевыми лимитами и настройками HTTP-клиента. Пять симптомов, с которыми чаще всего сталкиваются, и что с ними делать:
| Проблема | Причина | Решение |
|---|---|---|
| Все задачи заканчиваются таймаутом | Слишком высокий параллелизм перегружает CaptchaAI или прокси | Снизьте concurrency до 5–10 |
ERR_SOCKET_EXHAUSTION |
Слишком много одновременных HTTP-соединений | Используйте http.Agent с лимитом maxSockets |
| Результаты приходят не в том порядке | Асинхронные задачи завершаются не в порядке отправки | Сохраняйте результат по индексу задачи (см. пример выше) |
| Память растёт на больших пакетах | Все промисы одновременно хранятся в памяти | Обрабатывайте задачи блоками по 100–500 |
Много ошибок ERROR_NO_SLOT_AVAILABLE |
concurrency превышает число потоков тарифного плана |
Снизьте параллелизм до лимита потоков или перейдите на тариф с их бо́льшим числом |
Частые вопросы
Как понять, какой тариф CaptchaAI нужен под нужный параллелизм?
Ориентируйтесь на желаемый параллелизм, а не на число CAPTCHA в месяц — CaptchaAI тарифицирует по потокам с неограниченным числом решений на поток. Для 10 параллельных задач хватает STANDARD ($30/мес, 15 потоков); для 50 — ADVANCE ($90/мес, 50 потоков).
Сколько потоков параллелизма выбрать для старта?
Начните с 10 и увеличивайте, пока не увидите рост доли ошибок или замедление ответов. Большинство пайплайнов стабильно работают на 10–50 одновременных решениях — дальше вы упираетесь в лимит потоков тарифа, а не в производительность кода.
Что делать, если после Promise.allSettled часть задач падает с ERROR_NO_SLOT_AVAILABLE?
Это значит, что параллелизм в коде превысил число потоков вашего тарифа. Добавьте ERROR_NO_SLOT_AVAILABLE в список повторяемых ошибок (как в примере с batchSolveWithRetry) и одновременно снизьте concurrency либо увеличьте число потоков плана.
Чем Promise.allSettled отличается от очереди на воркерах при непрерывном парсинге?
Promise.allSettled рассчитан на пакет с известным концом: вы отправляете фиксированный список задач и дожидаетесь всех результатов разом. Для бесконечного потока задач — например, постоянного парсинга страниц — удобнее очередь на воркерах, которая берёт новую задачу по мере освобождения потока, а не ждёт завершения всего пакета.