Tutorials

Manajemen Status Sesi CAPTCHA di Seluruh Worker Terdistribusi

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.

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.

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:

Komentar dinonaktifkan untuk artikel ini.