Baru kirim 50 CAPTCHA sekaligus dari Node.js, lalu satu task gagal dan seluruh batch ikut ambruk? Itu bukan bug di kode Anda — begitu cara kerja Promise.all: begitu satu promise reject, semua hasil yang belum selesai langsung hilang, termasuk yang sebenarnya sudah berhasil. Promise.allSettled menyelesaikan masalah ini dengan menunggu setiap task sampai tuntas, sukses maupun gagal, lalu mengembalikan status masing-masing satu per satu.
Tiga alasan kenapa pola ini penting khusus untuk batch CAPTCHA, bukan sekadar preferensi gaya kode:
- Kegagalan sebagian itu normal — proxy timeout, sitekey salah, atau slot API penuh terjadi tiap hari, bukan sesekali.
- Anda tetap butuh hasil yang sudah berhasil disimpan, bukan cuma notifikasi bahwa batch-nya gagal.
- Retry seharusnya menyasar task yang gagal saja, bukan mengulang seluruh batch dari nol.
Kalau Anda baru migrasi dari
Promise.all, cek dulu apakah ada kode yang bergantung pada batch "semua atau tidak sama sekali" —Promise.allSettledtidak pernah throw, jadi logic yang menangkap error di level batch perlu disesuaikan.
Promise.all vs Promise.allSettled: Bedanya untuk Batch CAPTCHA
Sebelum masuk ke implementasi, pahami dulu kenapa Promise.all berisiko untuk batch solve: satu kegagalan membuat Anda kehilangan visibility ke semua task lain yang sebenarnya sudah fulfilled. Tabel berikut merangkum perbedaannya:
// 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}]
| Metode | Pada kegagalan pertama | Kembali | Terbaik untuk |
|---|---|---|---|
Promise.all |
Segera menolak | Tidak ada (melempar) | Operasi semua atau tidak sama sekali |
Promise.allSettled |
Berlanjut | Setiap hasil | Pemecahan CAPTCHA secara batch |
Kode di bawah menunjukkan pola lengkapnya — dari pengiriman satu CAPTCHA sampai penggabungan hasil batch penuh.
Cara Kerja: Kirim, Polling, lalu Tangkap Tiap Hasil
Fungsi solveCaptcha menangani satu siklus penuh: kirim task ke in.php, polling res.php setiap 5 detik hingga maksimal 60 kali percobaan sebelum menyerah dengan error TIMEOUT, lalu batchSolve membungkus semuanya dengan Promise.allSettled supaya kegagalan satu task tidak menghentikan yang lain.
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}`);
}
})();
Contoh tasks di atas pakai sitekey reCAPTCHA v2 sebagai ilustrasi — pola submit-lalu-poll yang sama berlaku untuk tipe lain yang didukung CaptchaAI, seperti Cloudflare Turnstile atau GeeTest v3, tinggal ganti parameter method.
Batasi Concurrency Supaya Tidak Overload
Mengirim 1.000 CAPTCHA bersamaan tanpa batas membebani koneksi HTTP Anda sendiri, bukan cuma API CaptchaAI. batchSolveWithLimit di bawah membatasi jumlah task yang berjalan bersamaan lewat pola worker pool sederhana — sejumlah "worker" menarik task berikutnya dari antrean begitu task sebelumnya selesai.
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);
Samakan Concurrency dengan Plan Thread CaptchaAI
Angka concurrency di kode bukan sekadar parameter teknis — ia berhubungan langsung dengan jumlah thread yang Anda bayar. CaptchaAI menagih per thread konkuren, bukan per solve, jadi menjalankan concurrency = 10 berarti Anda idealnya punya minimal 10 thread aktif supaya semua task jalan bersamaan tanpa antre di sisi Anda sendiri. Patokan kasar mencocokkan concurrency dengan plan:
- Batch dengan
concurrency≤ 15 task paralel — STANDARD ($30/bulan, 15 thread) sudah cukup dengan margin. - Batch dengan
concurrency16–50 task paralel — ADVANCE ($90/bulan, 50 thread). - Batch dengan
concurrencydi atas 50 — pertimbangkan PREMIUM ($170/bulan, 100 thread) atau CORPORATE ($240/bulan, 150 thread) supaya tidak ada task yang antre di kode padahal thread masih tersedia.
Bagi tim otomatisasi dan scraping lepas di Indonesia yang biasa mengelola beberapa proyek klien sekaligus, model thread-based ini relevan karena biayanya tetap flat berapa pun jumlah solve dalam sebulan — beda dengan skema per-solve yang membengkak begitu volume naik. Kalau infrastruktur Anda deploy di region seperti AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta), latensi ke API CaptchaAI relatif kecil, tapi tetap sisakan margin timeout untuk koneksi yang kurang stabil saat development di jaringan mobile-first.
Retry Otomatis untuk Task yang Gagal
Sebagian kegagalan bersifat sementara — TIMEOUT, ERROR_NO_SLOT_AVAILABLE, atau ERROR_TOO_MUCH_REQUESTS biasanya hilang begitu dicoba ulang. batchSolveWithRetry di bawah mengulang khusus task yang gagal karena alasan tersebut, dan membiarkan error permanen — misalnya sitekey yang salah — apa adanya tanpa retry sia-sia.
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 };
}
Pantau Progress Batch Secara Real-Time
Untuk batch besar, menunggu tanpa indikator progress terasa seperti proses yang macet. batchSolveWithProgress mencetak ulang baris yang sama di terminal setiap kali satu task selesai, jadi Anda tahu berapa yang sudah sukses dan berapa yang gagal tanpa menunggu seluruh batch selesai lebih dulu.
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;
}
Kelompokkan Hasil Biar Gampang Ditindaklanjuti
Hasil mentah Promise.allSettled masih berupa array fulfilled/rejected generik. categorizeResults memisahkannya menjadi tiga kelompok yang bisa langsung ditindaklanjuti: task yang sudah selesai, error sementara yang layak di-retry, dan error permanen yang butuh perhatian manual — misalnya sitekey yang sudah tidak valid.
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;
}
Masalah Umum Saat Batch Solve Berjalan
Beberapa masalah muncul berulang begitu batch mulai berjalan di volume produksi, bukan cuma di percobaan kecil:
| Masalah | Sebab | Perbaiki |
|---|---|---|
| Batas waktu semua tugas habis | Terlalu banyak permintaan bersamaan yang membebani CaptchaAI atau proxy | Kurangi konkurensi menjadi 5-10 |
ERR_SOCKET_EXHAUSTION |
Terlalu banyak koneksi HTTP simultan | Gunakan http.Agent dengan batas maxSockets |
| Rangkaian hasil tidak berurutan | Urutan penyelesaian asinkron berbeda dengan pengiriman | Gunakan penyimpanan hasil berbasis indeks (ditampilkan di atas) |
| Memori bertambah selama batch besar | Menyimpan semua janji dalam ingatan | Proses dalam potongan 100-500 |
Pertanyaan Umum
Berapa banyak thread CaptchaAI yang saya butuhkan untuk batch 100 CAPTCHA sekaligus?
Idealnya samakan dengan angka concurrency yang Anda pakai di kode. Batch 100 task dengan concurrency = 10 hanya butuh 10 thread aktif — plan STANDARD ($30/bulan, 15 thread) sudah cukup dengan margin. Kalau Anda menaikkan concurrency ke 50, pertimbangkan ADVANCE ($90/bulan, 50 thread) supaya tidak ada task yang antre di sisi kode padahal API masih punya slot kosong.
Concurrency berapa yang paling aman untuk Promise.allSettled?
Mulai dari 10 task bersamaan, lalu naikkan bertahap sambil memperhatikan error rate. Sebagian besar setup stabil di rentang 10–50 solve bersamaan; di atas itu, koneksi HTTP Anda sendiri biasanya jadi bottleneck lebih dulu dibanding API CaptchaAI.
Perlukah Promise.allSettled dipakai untuk batch di bawah 5 CAPTCHA?
Perbedaannya memang nyaris tidak terasa untuk batch sekecil itu, tapi tetap pakai Promise.allSettled sebagai kebiasaan default. Begitu batch Anda tumbuh dari 5 jadi 500 task, Anda tidak perlu mengganti pola sama sekali.
Bagaimana menangani error ERROR_NO_SLOT_AVAILABLE saat traffic batch sedang tinggi?
Error ini bersifat sementara — retry logic pada batchSolveWithRetry di atas sudah menanganinya secara otomatis. Kalau errornya sering muncul, itu biasanya sinyal untuk menurunkan angka concurrency atau menaikkan alokasi thread plan Anda, bukan sekadar menambah jumlah percobaan retry.
Amankah menjalankan ribuan task Promise.allSettled dalam satu proses Node.js?
Aman, selama Anda tetap membatasi concurrency dan memproses dalam potongan (chunk) 100-500 task seperti pada tabel di atas. Menyimpan puluhan ribu Promise sekaligus di memori adalah penyebab paling umum proses Node.js kehabisan resource pada batch besar.
Langkah Selanjutnya
Solve CAPTCHA secara paralel tanpa kehilangan hasil sebagian — ambil API key CaptchaAI Anda dan jalankan batchSolveWithLimit pada task pertama Anda.
Panduan terkait: