Satu CAPTCHA yang gagal di-solve tidak seharusnya menghentikan seluruh job scraping semalaman. Kalau automation Anda langsung crash begitu API mengembalikan ERROR_ZERO_BALANCE atau timeout CAPCHA_NOT_READY, ribuan URL yang sudah antre ikut batal diproses padahal masalahnya cuma satu task. Graceful degradation menahan kegagalan itu di satu titik saja: task yang gagal dilewati, di-retry nanti, atau dialihkan ke mode terbatas — sementara task lain tetap berjalan seperti biasa.
Ragam Kegagalan CAPTCHA Solving dan Kode Errornya
Sebelum menulis pola pemulihan, kenali dulu jenis kegagalan yang mungkin muncul dari API CaptchaAI. Setiap kode error butuh strategi pemulihan yang berbeda — retry buta untuk semua jenis kegagalan justru membuang saldo thread tanpa hasil.
| Kegagalan | Kode Error | Strategi Pemulihan |
|---|---|---|
| Timeout polling | CAPCHA_NOT_READY (melebihi batas polling) |
Retry dengan challenge baru |
| Parameter salah | ERROR_BAD_PARAMETERS |
Log lalu lewati — perbaiki dulu proses ekstraksi |
| Sitekey keliru | ERROR_WRONG_GOOGLEKEY |
Ekstrak ulang sitekey dari halaman |
| Saldo habis | ERROR_ZERO_BALANCE |
Jeda proses, kirim alert, tunggu top up |
| Rate limit | ERROR_TOO_MUCH_REQUESTS |
Exponential backoff |
| API tidak merespons | Connection error | Circuit breaker + retry terjadwal |
Pola 1: Lewati Task yang Gagal, Jangan Hentikan Seluruh Batch
Untuk proses batch — misalnya scraping ratusan halaman produk semalaman — kegagalan pada satu URL tidak boleh menghentikan URL berikutnya. Pola paling sederhana: retry beberapa kali dengan batas maksimum, lalu kalau tetap gagal, catat alasannya dan lanjut ke item berikutnya. Fungsi solve_or_skip di bawah mengembalikan None alih-alih melempar exception, sehingga loop utama tidak pernah berhenti hanya karena satu task bermasalah.
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_or_skip(captcha_type, sitekey, page_url, max_retries=2):
"""Try to solve; return None on failure instead of crashing."""
for attempt in range(max_retries):
try:
token = solve_captcha(captcha_type, sitekey, page_url)
if token:
return token
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
return None # Skip this item
def process_urls(urls):
results = []
skipped = []
for url in urls:
sitekey = extract_sitekey(url)
if not sitekey:
skipped.append({"url": url, "reason": "no_sitekey"})
continue
token = solve_or_skip("recaptcha_v2", sitekey, url)
if token:
data = submit_form(url, token)
results.append({"url": url, "data": data})
else:
skipped.append({"url": url, "reason": "solve_failed"})
print(f"Processed: {len(results)}, Skipped: {len(skipped)}")
return results, skipped
Hasil akhirnya dua daftar terpisah: results untuk task yang berhasil dan skipped untuk yang gagal beserta alasannya — cukup untuk investigasi manual tanpa menghentikan proses yang sedang berjalan.
Pola 2: Retry Queue — Coba Lagi Nanti, Bukan Sekarang
Skip saja terlalu boros kalau kegagalannya sifatnya sementara, misalnya rate limit atau gangguan jaringan sesaat. Solusinya: task yang gagal masuk ke retry queue dengan exponential backoff, lalu diproses ulang secara berkala oleh proses terpisah. RetryQueue di bawah menyimpan retry_count per task dan menghitung retry_after berdasarkan backoff_base — makin sering gagal, makin lama jeda sebelum dicoba lagi.
from collections import deque
import json
class RetryQueue:
def __init__(self, max_retries=3, backoff_base=60):
self.queue = deque()
self.max_retries = max_retries
self.backoff_base = backoff_base
def add(self, task):
task["retry_count"] = task.get("retry_count", 0) + 1
if task["retry_count"] <= self.max_retries:
task["retry_after"] = time.time() + (
self.backoff_base * task["retry_count"]
)
self.queue.append(task)
return True
return False # Exceeded max retries
def get_ready(self):
"""Get tasks ready for retry."""
ready = []
remaining = deque()
now = time.time()
while self.queue:
task = self.queue.popleft()
if task["retry_after"] <= now:
ready.append(task)
else:
remaining.append(task)
self.queue = remaining
return ready
def save(self, filepath="retry_queue.json"):
with open(filepath, "w") as f:
json.dump(list(self.queue), f)
def load(self, filepath="retry_queue.json"):
try:
with open(filepath) as f:
self.queue = deque(json.load(f))
except FileNotFoundError:
pass
# Usage
retry_q = RetryQueue()
def process_with_retry(task):
try:
token = solve_captcha(task["type"], task["sitekey"], task["url"])
if token:
return submit_form(task["url"], token)
else:
retry_q.add(task)
except Exception:
retry_q.add(task)
# Process retry queue periodically
def drain_retry_queue():
ready = retry_q.get_ready()
for task in ready:
process_with_retry(task)
Panggil drain_retry_queue() lewat cron job atau scheduler setiap beberapa menit. Method save() dan load() di kelas ini penting kalau proses bisa restart — tanpa persistensi, seluruh antrean retry yang tersimpan di memory hilang begitu proses berhenti.
Pola 3: Degraded Mode Saat Layanan Solving Bermasalah
Kalau kegagalan API bukan cuma satu-dua task tapi beruntun — misalnya lima kali berturut-turut — kemungkinan besar masalahnya ada di layanan solving itu sendiri, bukan di task individual. Di titik ini retry per-task tidak banyak membantu; yang dibutuhkan adalah circuit breaker: hentikan sementara panggilan API, masuk ke degraded mode selama beberapa menit, lalu coba pulih otomatis.
class CaptchaSolver:
def __init__(self, api_key):
self.api_key = api_key
self.degraded = False
self.failure_count = 0
self.failure_threshold = 5
self.recovery_time = None
def solve(self, captcha_type, sitekey, page_url):
if self.degraded:
if time.time() < self.recovery_time:
return self._degraded_action(page_url)
else:
self.degraded = False
self.failure_count = 0
try:
token = self._solve_api(captcha_type, sitekey, page_url)
self.failure_count = 0
return token
except Exception as e:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self._enter_degraded_mode()
raise
def _enter_degraded_mode(self):
self.degraded = True
self.recovery_time = time.time() + 300 # 5 min
print("Entering degraded mode for 5 minutes")
# Send alert
def _degraded_action(self, url):
"""What to do when solving is unavailable."""
# Option A: Skip CAPTCHA pages entirely
return None
# Option B: Queue for later
# retry_queue.add({"url": url, ...})
# return None
# Option C: Try alternative solver
# return self._solve_with_backup_api(...)
def _solve_api(self, captcha_type, sitekey, page_url):
# Normal CaptchaAI API call
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}).json()
if resp["status"] != 1:
raise Exception(resp["request"])
task_id = resp["request"]
for _ in range(24):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": "1"
}).json()
if result["status"] == 1:
return result["request"]
if result["request"] != "CAPCHA_NOT_READY":
raise Exception(result["request"])
raise Exception("TIMEOUT")
_degraded_action() sengaja dibiarkan sebagai tiga opsi: lewati saja, masukkan ke retry queue (gabungkan dengan Pola 2), atau alihkan ke solver cadangan. Pilihannya tergantung SLA automation Anda — untuk QA testing internal, skip biasanya cukup; untuk pipeline produksi yang harus tetap menghasilkan data, gabungkan dengan retry queue.
Implementasi Node.js: Menggabungkan Ketiga Pola
Versi Node.js ini menyatukan retry queue dan degraded mode dalam satu class. Begitu ERROR_ZERO_BALANCE muncul, ResilientSolver langsung masuk degraded mode selama 10 menit tanpa menunggu counter kegagalan — saldo nol tidak akan pulih sendiri hanya karena dicoba ulang. Kegagalan jenis lain baru memicu degraded mode setelah lima kali gagal berturut-turut.
class ResilientSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.retryQueue = [];
this.failureCount = 0;
this.degraded = false;
}
async solve(type, sitekey, pageUrl) {
if (this.degraded) {
this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
return null;
}
try {
const token = await this._callApi(type, sitekey, pageUrl);
this.failureCount = 0;
return token;
} catch (err) {
this.failureCount++;
if (err.message === 'ERROR_ZERO_BALANCE') {
this._enterDegraded(600000); // 10 min
return null;
}
if (this.failureCount >= 5) {
this._enterDegraded(300000); // 5 min
}
this.retryQueue.push({ type, sitekey, pageUrl, addedAt: Date.now() });
return null;
}
}
_enterDegraded(durationMs) {
this.degraded = true;
console.warn(`Degraded mode for ${durationMs / 1000}s`);
setTimeout(() => {
this.degraded = false;
this.failureCount = 0;
this.drainRetryQueue();
}, durationMs);
}
async drainRetryQueue() {
const tasks = this.retryQueue.splice(0);
for (const task of tasks) {
await this.solve(task.type, task.sitekey, task.pageUrl);
}
}
async _callApi(type, sitekey, pageUrl) {
// Standard submit + poll
const axios = require('axios');
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: { key: this.apiKey, method: 'userrecaptcha', googlekey: sitekey, pageurl: pageUrl, json: 1 },
});
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: this.apiKey, action: 'get', id: taskId, json: 1 },
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
}
throw new Error('TIMEOUT');
}
}
Kapan Retry, Skip, atau Alihkan ke Degraded Mode?
Tiga pola di atas gampang tertukar penggunaannya. Aturan praktis yang bisa dipakai tim automation:
- Retry hanya untuk kegagalan yang sifatnya sementara dan status sesi di sekitarnya masih valid — timeout, rate limit, atau gangguan koneksi singkat.
- Replay sebuah langkah hanya jika request itu aman diulang tanpa menduplikasi tindakan user — submit form dua kali dengan token yang sama biasanya ditolak server, bukan diproses dua kali.
- Skip kalau errornya deterministik, seperti
ERROR_BAD_PARAMETERS— mengulang task yang sama dengan parameter yang salah hanya membuang thread tanpa hasil. - Degraded mode kalau kegagalan beruntun menunjukkan masalah di layanan, bukan di task — masuk mode terbatas mencegah ratusan task gagal bersamaan hanya karena API sedang bermasalah sesaat.
Studi Kasus: Agensi Pemantauan Harga E-Commerce
Skenario yang umum di agensi pemantauan harga e-commerce Indonesia: job cron jalan tiap malam, memproses ribuan halaman produk lewat worker yang di-deploy di region ap-southeast-1 (Singapura) atau asia-southeast2 (Jakarta) supaya latensinya rendah. Kalau satu halaman gagal solve CAPTCHA dan tidak ada graceful degradation, seluruh job semalaman bisa berhenti — padahal biaya thread-nya sudah berjalan. Karena CaptchaAI menagih per thread yang dipakai bersamaan, bukan per solve, task yang di-skip atau masuk retry queue tidak menambah biaya ekstra; yang justru membuang biaya adalah job yang crash di tengah jalan dan harus diulang dari nol keesokan harinya.
Kalau data yang di-scrape berpotensi memuat informasi pribadi — nama, alamat, atau nomor telepon di halaman checkout, misalnya — pastikan proses hanya mengambil data yang memang berwenang diproses tim Anda. Ini relevan dengan UU Pelindungan Data Pribadi (UU 27/2022), bukan cuma soal graceful degradation teknis.
Masalah Umum dan Solusinya
Beberapa masalah paling sering muncul saat tim baru menerapkan pola ini:
| Masalah | Penyebab | Solusi |
|---|---|---|
| Semua task ikut dilewati | Degraded mode terlalu sensitif — threshold kegagalan terlalu rendah | Naikkan failure_threshold |
| Retry queue terus membengkak | Task yang gagal memang tidak pernah bisa berhasil | Tetapkan batas retry maksimum; pindahkan ke dead-letter queue |
| Pemulihan lambat setelah masalah selesai | Recovery time degraded mode diset terlalu lama | Perkecil recovery time; tambahkan health check probe |
| Queue hilang setelah restart | Queue cuma disimpan di memory | Persiste ke file atau database |
Pertanyaan Umum
Apa beda graceful degradation dengan circuit breaker?
Circuit breaker menghentikan panggilan API sepenuhnya begitu kegagalan terdeteksi. Graceful degradation lebih luas — mencakup skip logic, retry queue, dan fallback workflow, dengan circuit breaker sebagai salah satu komponennya. Keduanya biasanya dipakai bersama, bukan sebagai pengganti satu sama lain.
Berapa nilai failure_threshold yang wajar sebelum masuk degraded mode?
Tidak ada angka baku, tapi lima kegagalan berturut-turut (seperti contoh kode di atas) adalah titik awal yang aman untuk sebagian besar automation. Threshold terlalu rendah membuat automation terlalu cepat menyerah pada gangguan sesaat; terlalu tinggi berarti Anda membuang banyak thread sebelum sistem sadar API sedang bermasalah.
Retry queue sebaiknya disimpan di file, database, atau cukup in-memory?
In-memory cukup untuk proses jangka pendek yang tidak pernah restart di tengah jalan. Untuk job produksi yang berjalan berjam-jam atau bisa di-restart kapan saja, simpan ke file JSON (seperti save()/load() di atas) atau database — kalau tidak, seluruh antrean retry hilang begitu proses berhenti.
Apakah retry menambah biaya API CaptchaAI?
Tidak. Karena CaptchaAI menagih per thread yang dipakai bersamaan, bukan per solve, task yang di-retry tidak menambah biaya ekstra selama masih dalam alokasi thread paket Anda. Yang perlu diperhatikan justru waktu — retry yang terlalu agresif memenuhi slot thread dengan task yang kemungkinan besar akan gagal lagi.