Gửi 50 CAPTCHA cùng lúc bằng Promise.all? Chỉ cần 1 task lỗi giữa chừng là toàn bộ 49 kết quả còn lại — kể cả những cái đã giải xong — bị hủy theo, vì Promise.all từ chối (reject) ngay khi gặp lỗi đầu tiên. Promise.allSettled không có nhược điểm này: nó đợi tất cả promise chạy xong, dù thành công hay thất bại, rồi trả về trạng thái riêng của từng task. Với batch vài trăm đến vài nghìn CAPTCHA mỗi lần chạy, đây gần như bắt buộc.
Ví dụ thực tế: một team QA outsourcing ở TP.HCM cần giải khoảng 2.000 reCAPTCHA v2 trên trang staging mỗi đêm để kiểm thử pipeline crawl catalog kiểu Shopee/Lazada trước khi đẩy lên production. Dùng
Promise.all, một request timeout giữa batch khiến cả 2.000 task chạy lại từ đầu. VớiPromise.allSettled, script giữ nguyên 1.999 kết quả đã giải, chỉ cần retry đúng task lỗi.
Bốn bước không đổi dù bạn giải 1 CAPTCHA hay 10.000:
- Gửi task tới
in.php(kèmsitekey,pageurl) - Nhận
idtask từ response - Polling
res.phptheo chu kỳ cho đến khi có kết quả - Dùng
tokentrả về để submit form
So sánh Promise.all và Promise.allSettled
// 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}]
| Phương pháp | Khi có task đầu tiên lỗi | Giá trị trả về | Phù hợp với |
|---|---|---|---|
Promise.all |
Từ chối (reject) ngay lập tức | Không có gì — ném exception | Thao tác kiểu tất cả-hoặc-không-gì |
Promise.allSettled |
Tiếp tục chạy các task còn lại | Toàn bộ kết quả, kèm trạng thái từng task | Giải CAPTCHA hàng loạt |
Khác biệt nằm ở đúng một chỗ: Promise.allSettled không bao giờ reject cả mảng — mỗi phần tử trả về là một object {status, value} hoặc {status, reason}, nên bạn luôn biết chính xác task nào giải được và task nào cần retry.
Hàm giải CAPTCHA dùng cho batch
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}`);
}
})();
Giới hạn concurrency theo số thread đã mua
solveCaptcha() và batchSolve() ở trên đã đủ để chạy một batch nhỏ, nhưng gửi thẳng 1.000 request cùng lúc sẽ làm quá tải kết nối HTTP phía client lẫn hàng đợi phía CaptchaAI. CaptchaAI tính phí theo thread (số CAPTCHA giải đồng thời), không theo số lượt giải, nên concurrency trong code nên khớp với số thread của gói đang dùng:
- BASIC — $15/tháng, 5 thread
- ADVANCE — $90/tháng, 50 thread
- ENTERPRISE — $300/tháng, 200 thread
Đặt concurrency cao hơn số thread đã mua không giúp giải nhanh hơn — các request thừa chỉ xếp hàng chờ đến khi có thread trống. Dùng bộ giới hạn đồng thời sau:
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);
Retry tự động cho lỗi tạm thời
Không phải task lỗi nào cũng nên retry — tách rõ hai nhóm trước khi gửi lại:
- Lỗi tạm thời (nên retry):
TIMEOUT,ERROR_NO_SLOT_AVAILABLE,ERROR_TOO_MUCH_REQUESTS - Lỗi vĩnh viễn (không nên retry): sai
sitekey/googlekey
Hàm dưới đây tự động lọc theo đúng phân loại này:
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 };
}
Hiển thị tiến độ khi batch chạy lâu
Với batch vài trăm task, script chạy im lặng vài phút khiến khó biết đang treo hay đang chạy bình thường. In tiến độ theo thời gian thực giúp phát hiện sớm nếu tỷ lệ lỗi tăng bất thường:
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;
}
Phân loại kết quả để xử lý tiếp
Đừng chỉ tách "thành công / thất bại" — chia kết quả thành ba nhóm để bước retry ở trên có input dùng ngay:
solved— task đã cótokentransientErrors— nên retry (timeout, hết slot, quá request)permanentErrors— lỗi cấu hình, retry cũng vô ích
Hàm dưới đây trả về đúng ba nhóm này:
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;
}
Khắc phục sự cố thường gặp
Các lỗi hay gặp nhất khi chạy batch ở quy mô vài trăm đến vài nghìn task, xếp theo mức độ phổ biến:
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Toàn bộ task đều timeout | Quá nhiều request đồng thời áp đảo CaptchaAI hoặc proxy | Giảm concurrency xuống 5–10 |
ERR_SOCKET_EXHAUSTION |
Quá nhiều kết nối HTTP mở cùng lúc | Dùng http.Agent với giới hạn maxSockets |
| Mảng kết quả không đúng thứ tự gửi | Task hoàn thành không đồng bộ, không theo thứ tự submit | Lưu kết quả theo chỉ mục (index) như code phần "Giới hạn concurrency" ở trên |
| Bộ nhớ tăng dần khi batch lớn | Giữ toàn bộ promise đang chờ trong bộ nhớ cùng lúc | Chia batch thành các khối 100–500 task rồi xử lý tuần tự từng khối |
| Retry vẫn lỗi liên tục | Đang retry cả lỗi vĩnh viễn (sai sitekey/googlekey) chung với lỗi tạm thời | Chỉ retry TIMEOUT, ERROR_NO_SLOT_AVAILABLE, ERROR_TOO_MUCH_REQUESTS như hàm batchSolveWithRetry |
Câu hỏi thường gặp
Nên đặt concurrency bao nhiêu khi giải CAPTCHA hàng loạt?
Bắt đầu ở mức 10 task đồng thời, tăng dần tới khi thấy lợi nhuận giảm hoặc tỷ lệ lỗi leo lên. Đa số chạy tốt ở 10–50, nhưng mức trần thực tế là số thread trong gói CaptchaAI đang dùng.
Promise.allSettled có giữ đúng thứ tự kết quả theo thứ tự gửi không?
Có, nếu bạn dùng Promise.allSettled(tasks.map(...)) — mảng kết quả trả về luôn khớp vị trí với mảng promise đầu vào, bất kể task nào hoàn thành trước. Vấn đề thứ tự chỉ xảy ra khi tự quản lý worker pool thủ công (như batchSolveWithLimit ở trên) mà quên ghi kết quả theo đúng chỉ mục gốc.
Giải 10.000 CAPTCHA bằng batch này tốn khoảng bao nhiêu?
Tùy gói đang dùng — CaptchaAI tính theo thread, không theo lượt giải. Với ADVANCE ($90/tháng, 50 thread), bạn giải không giới hạn số lượng miễn không vượt 50 task đồng thời. Xem chi tiết tại trang giá CaptchaAI.
Khi nào nên chuyển sang worker queue (BullMQ, Redis) thay vì Promise.allSettled?
Khi CAPTCHA đến liên tục thay vì theo đợt cố định — ví dụ crawler chạy 24/7 chứ không phải job batch chạy một lần rồi kết thúc. Promise.allSettled phù hợp cho gửi-và-chờ một lần; với luồng task vô hạn, hàng đợi có backoff và retry bền như BullMQ quản lý tài nguyên tốt hơn.
Bước tiếp theo
Lấy API key CaptchaAI và áp dụng Promise.allSettled cho batch giải CAPTCHA đầu tiên — bắt đầu với concurrency thấp rồi tăng dần theo số thread trong gói đang dùng.
Hướng dẫn liên quan: