Begitu Anda menjalankan lebih dari satu worker CAPTCHA untuk domain yang sama, jawabannya sederhana: worker-worker itu wajib berbagi satu session store terpusat — biasanya Redis — bukan menyimpan cookie dan token secara lokal di tiap proses. Tanpa itu, setiap worker punya sesinya sendiri: cookie berbeda, IP berbeda, sinyal browser berbeda. Situs target melihat pola tidak konsisten, dan sebagian solve CAPTCHA terbuang percuma karena dipakai pada sesi yang sudah kedaluwarsa. Panduan ini membahas elemen state yang perlu disinkronkan, arsitektur Redis-nya, implementasi Python dan JavaScript siap pakai, serta pola umum untuk session lock, token pool, dan proxy pool.
Kenapa Worker CAPTCHA Butuh State Bersama, Bukan Sesi Lokal
Ilustrasi berikut menunjukkan masalah klasik saat tiga worker berjalan tanpa koordinasi:
Worker 1 → Login → Solve CAPTCHA → Get cookie A → Submit form ✅
Worker 2 → New session → Solve CAPTCHA → Get cookie B → Submit form ✅
Worker 3 → Reuse cookie A? → Cookie expired → Solve CAPTCHA → Fail ❌
Worker 3 gagal bukan karena CAPTCHA-nya sulit, tapi karena cookie yang dipakai ulang sudah kedaluwarsa. Tanpa state bersama, setiap worker mengautentikasi ulang dari nol, menghabiskan thread CaptchaAI untuk solve yang sebenarnya bisa dihindari, dan menghasilkan trafik yang mudah dicurigai bot karena polanya tidak konsisten antar request.
Elemen State yang Perlu Disinkronkan Antar Worker
Tidak semua data sesi dibagi dengan cara sama — sebagian butuh TTL pendek, sebagian tidak boleh dibagi sama sekali:
| Elemen State | Masa Berlaku | Cara Berbagi |
|---|---|---|
| Cookie autentikasi | Menit hingga jam | Redis dengan TTL |
| Token CAPTCHA | 90–300 detik | List Redis (TTL pendek) |
Cookie qa_validation_cookie |
~30 menit | Redis hash |
| Token CSRF | Per page load | Jangan dibagi — tiap worker ambil sendiri |
| Sinyal browser | Permanen | Bagian konfigurasi, bukan state runtime |
| Penugasan proxy | Per sesi | Pool proxy berbasis Redis |
Arsitektur Redis untuk Session State Terpusat
Semua worker terhubung ke satu instance Redis yang menyimpan empat jenis data: cookie per domain, token per sitekey, pool proxy, dan lock sesi.
┌──────────────────────────────────────┐
│ Session State Store │
│ (Redis) │
│ │
│ cookies:{domain} → Hash │
│ tokens:{sitekey} → List │
│ proxies:pool → Set │
│ locks:{domain}:{worker} → String │
└─────┬──────────┬──────────┬──────────┘
│ │ │
┌───▼───┐ ┌──▼────┐ ┌──▼────┐
│Worker1│ │Worker2│ │Worker3│
└───────┘ └───────┘ └───────┘
Struktur key ini membuat worker mana pun bisa query state tanpa tahu worker lain sedang apa — Redis jadi satu-satunya sumber kebenaran untuk sesi aktif.
Implementasi Python: Session Store, Worker, dan Proxy Pool
Tiga komponen saling melengkapi: session store menyimpan state, worker memakainya untuk solve dan submit, proxy pool mencegah rebutan IP.
Session Store: Simpan dan Ambil Cookie serta Token
import os
import json
import time
import redis
import requests
from datetime import datetime, timezone
r = redis.Redis(
host=os.environ.get("REDIS_HOST", "localhost"),
port=int(os.environ.get("REDIS_PORT", 6379)),
decode_responses=True
)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
class SessionStore:
"""Shared session state across distributed workers."""
def __init__(self, domain):
self.domain = domain
self.cookie_key = f"session:cookies:{domain}"
self.token_key = f"session:tokens:{domain}"
def save_cookies(self, cookies, ttl=1800):
"""Store cookies from a successful session."""
cookie_data = {name: value for name, value in cookies.items()}
r.hset(self.cookie_key, mapping=cookie_data)
r.expire(self.cookie_key, ttl)
def get_cookies(self):
"""Retrieve shared cookies."""
cookies = r.hgetall(self.cookie_key)
return cookies if cookies else None
def save_token(self, sitekey, token, ttl=80):
"""Store a solved CAPTCHA token."""
key = f"{self.token_key}:{sitekey}"
r.rpush(key, token)
r.expire(key, ttl)
def get_token(self, sitekey):
"""Pop a cached CAPTCHA token."""
key = f"{self.token_key}:{sitekey}"
return r.lpop(key)
def acquire_session_lock(self, worker_id, ttl=300):
"""Ensure only one worker manages the session at a time."""
lock_key = f"session:lock:{self.domain}"
return r.set(lock_key, worker_id, nx=True, ex=ttl)
def release_session_lock(self, worker_id):
"""Release session lock if this worker holds it."""
lock_key = f"session:lock:{self.domain}"
current = r.get(lock_key)
if current == worker_id:
r.delete(lock_key)
SessionStore menangani empat operasi inti: save_cookies/get_cookies untuk cookie, save_token/get_token untuk token yang di-cache, dan sepasang fungsi lock supaya hanya satu worker mengelola re-autentikasi pada satu waktu.
Worker dengan State Bersama
Worker memakai SessionStore di tiga titik: muat cookie sebelum request, cek token cache sebelum memanggil API, simpan cookie baru setelah submit berhasil.
class CaptchaWorker:
def __init__(self, worker_id, domain):
self.worker_id = worker_id
self.store = SessionStore(domain)
self.session = requests.Session()
def setup_session(self):
"""Load shared cookies into this worker's session."""
cookies = self.store.get_cookies()
if cookies:
for name, value in cookies.items():
self.session.cookies.set(name, value)
return True
return False
def solve_captcha(self, sitekey, pageurl):
"""Solve with token cache and session sharing."""
# Check for cached token
cached = self.store.get_token(sitekey)
if cached:
return {"solution": cached, "source": "cache"}
# Solve via CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
return {"error": data.get("request")}
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
token = result["request"]
self.store.save_token(sitekey, token)
return {"solution": token, "source": "api"}
if result.get("request") != "CAPCHA_NOT_READY":
return {"error": result.get("request")}
return {"error": "TIMEOUT"}
def process_page(self, url, sitekey):
"""Full workflow: setup session → solve CAPTCHA → submit."""
# Load shared session
self.setup_session()
# Solve CAPTCHA
result = self.solve_captcha(sitekey, url)
if "error" in result:
return result
# Submit form with token
response = self.session.post(url, data={
"g-recaptcha-response": result["solution"]
})
# Share resulting cookies
self.store.save_cookies(dict(self.session.cookies))
return {"status": response.status_code, "source": result["source"]}
Token cache dicek lebih dulu lewat self.store.get_token(...), jadi worker hanya memanggil in.php kalau memang belum ada token yang bisa dipakai ulang.
Proxy Pool: Hindari Worker Berebut IP yang Sama
acquire_proxy memberi satu proxy per worker dan mencatatnya di Redis hash; release_proxy mengembalikannya ke pool begitu worker selesai memakainya.
class ProxyPool:
"""Distribute proxies across workers to avoid IP conflicts."""
def __init__(self, proxies):
self.pool_key = "session:proxy_pool"
self.assigned_key = "session:proxy_assigned"
# Initialize pool
for proxy in proxies:
r.sadd(self.pool_key, proxy)
def acquire_proxy(self, worker_id, ttl=600):
"""Assign an unused proxy to a worker."""
# Check if worker already has one
existing = r.hget(self.assigned_key, worker_id)
if existing:
return existing
# Pop from available pool
proxy = r.spop(self.pool_key)
if proxy:
r.hset(self.assigned_key, worker_id, proxy)
r.expire(self.assigned_key, ttl)
return proxy
return None
def release_proxy(self, worker_id):
"""Return proxy to the pool."""
proxy = r.hget(self.assigned_key, worker_id)
if proxy:
r.sadd(self.pool_key, proxy)
r.hdel(self.assigned_key, worker_id)
Kalau worker sudah punya proxy assigned, fungsi ini mengembalikan proxy yang sama, bukan mengambil yang baru — penting untuk situs yang melacak binding IP-sesi (lihat pola proxy affinity di bawah).
Implementasi JavaScript: Versi Node.js dengan ioredis
Logikanya identik dengan versi Python, hanya beda client Redis (ioredis) dan HTTP (axios):
const Redis = require("ioredis");
const axios = require("axios");
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
class SessionStore {
constructor(domain) {
this.domain = domain;
this.cookieKey = `session:cookies:${domain}`;
this.tokenKey = `session:tokens:${domain}`;
}
async saveCookies(cookies, ttl = 1800) {
const entries = Object.entries(cookies).flat();
if (entries.length > 0) {
await redis.hset(this.cookieKey, ...entries);
await redis.expire(this.cookieKey, ttl);
}
}
async getCookies() {
return await redis.hgetall(this.cookieKey);
}
async saveToken(sitekey, token, ttl = 80) {
const key = `${this.tokenKey}:${sitekey}`;
await redis.rpush(key, token);
await redis.expire(key, ttl);
}
async getToken(sitekey) {
return await redis.lpop(`${this.tokenKey}:${sitekey}`);
}
async acquireLock(workerId, ttl = 300) {
const result = await redis.set(`session:lock:${this.domain}`, workerId, "NX", "EX", ttl);
return result === "OK";
}
async releaseLock(workerId) {
const current = await redis.get(`session:lock:${this.domain}`);
if (current === workerId) await redis.del(`session:lock:${this.domain}`);
}
}
async function workerSolve(store, sitekey, pageurl) {
const cached = await store.getToken(sitekey);
if (cached) return { solution: cached, source: "cache" };
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) return { error: submit.data.request };
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
await store.saveToken(sitekey, poll.data.request);
return { solution: poll.data.request, source: "api" };
}
if (poll.data.request !== "CAPCHA_NOT_READY") return { error: poll.data.request };
}
return { error: "TIMEOUT" };
}
Perhatikan acquireLock memakai opsi NX dan EX bawaan Redis — mekanisme yang sama seperti nx=True, ex=ttl pada versi Python, dua bahasa dengan logika lock yang identik.
Skenario Nyata: Worker di Singapura dan Jakarta untuk Price Monitoring
Bayangkan tim price-monitoring lepas — kontraktor proyek scraping lewat Upwork atau Fastwork — menjalankan lima worker paralel: dua di AWS ap-southeast-1 (Singapura), tiga di GCP asia-southeast2 (Jakarta). Tanpa session store terpusat, kelima worker login terpisah dan memegang cookie sendiri; begitu satu sesi kedaluwarsa, worker itu solve ulang CAPTCHA dari nol meski worker lain masih punya sesi valid untuk domain yang sama. Dengan Redis sebagai session store bersama seperti pada implementasi di atas, worker yang lebih dulu mendapat sesi baru langsung membagikannya ke worker lain, dan re-solve sia-sia berkurang jauh. Karena CaptchaAI menagih per thread aktif, bukan per solve, solve yang tidak terbuang berarti lebih banyak thread tersisa untuk task baru — tim kecil bisa bertahan di STANDARD ($30/bulan, 15 thread) alih-alih naik ke ADVANCE ($90/bulan, 50 thread) hanya karena solve-nya boros. Catatan kepatuhan: batasi scraping hanya pada data yang memang berwenang Anda proses, mengingat UU Pelindungan Data Pribadi (UU 27/2022).
Pola Manajemen State: Kapan Pakai yang Mana
Empat pola berikut biasanya dikombinasikan, bukan dipilih salah satu:
| Pola | Kapan Dipakai |
|---|---|
| Session lock | Satu worker mengelola login, worker lain memakai cookie hasilnya |
| Token pool | Throughput tinggi — solve lebih awal, distribusikan token ke worker yang butuh |
| Cookie sharing | Worker perlu sesi yang sudah terautentikasi |
| Proxy affinity | Situs target melacak binding IP-sesi |
Pola yang sama berlaku untuk automasi berbasis browser (Puppeteer/Playwright): serialize cookie dengan page.cookies(), simpan ke Redis lewat SessionStore, muat kembali di worker lain dengan page.setCookie() — tidak perlu implementasi terpisah.
Mengatasi Masalah Sesi yang Umum Terjadi
Empat masalah paling sering dilaporkan tim yang baru memindahkan worker CAPTCHA ke Redis:
| Masalah | Penyebab | Solusi |
|---|---|---|
| Worker mendapat sesi berbeda-beda | Cookie tidak benar-benar tersimpan di Redis | Pastikan save_cookies dipanggil setelah setiap request yang berhasil |
| Token kedaluwarsa sebelum dipakai worker lain | TTL kepanjangan atau ada network delay | Perkecil margin TTL; pakai token dalam 10 detik sejak diambil |
| Session lock tidak pernah lepas | Worker crash sebelum sempat release | TTL pada lock key otomatis melepasnya (default 300 detik) |
| Situs target memblokir worker | Semua worker memakai proxy yang sama | Pakai proxy pool dengan affinity per worker |
Pertanyaan Seputar Session State Worker CAPTCHA
Berapa banyak worker sebaiknya berbagi satu Redis session store?
Tidak ada batas keras dari sisi Redis — satu instance kecil bisa melayani puluhan worker karena beban utamanya cuma baca/tulis key kecil. Batasnya ada di thread CaptchaAI: paket BASIC ($15/bulan, 5 thread) cocok untuk 2–3 worker, tim dengan belasan worker paralel biasanya sudah butuh ADVANCE ($90/bulan, 50 thread).
Apakah setup ini butuh proxy premium, atau IP datacenter biasa cukup?
Tergantung apakah situs target melacak binding IP-sesi. Kalau tidak, IP datacenter biasa dengan pool berbasis Redis sudah cukup. Kalau target melacak konsistensi IP per sesi, gunakan egress jaringan yang diotorisasi dan pertahankan satu proxy per worker selama sesi aktif.
Bagaimana menangani sesi yang kedaluwarsa saat traffic sedang tinggi?
Atur TTL Redis sedikit lebih pendek dari umur sesi sebenarnya. Saat cookie kedaluwarsa, satu worker mengambil session lock, re-autentikasi, lalu menyimpan cookie baru — worker lain tinggal membaca ulang dari Redis, tanpa perlu re-solve CAPTCHA masing-masing.
Apakah semua worker wajib berbagi cookie?
Tidak. Untuk solve CAPTCHA yang stateless — kirim sitekey, terima token, tanpa login — worker cukup berbagi token lewat pool Redis, tidak perlu cookie. Cookie sharing baru relevan untuk situs yang mewajibkan sesi terautentikasi sebelum submit form.
Langkah Berikutnya
Sinkronkan state sesi worker CAPTCHA Anda lewat Redis seperti pada implementasi di atas, lalu ambil API key CaptchaAI untuk mulai solve reCAPTCHA, Turnstile, dan tipe lain langsung dari worker yang sudah terkoordinasi.
Panduan terkait:
- cara mengatur TTL token Redis untuk CAPTCHA
- menjaga sesi browser tetap konsisten antar worker