Tutorials

Dağıtılmış Worker'larda CAPTCHA Oturum Durumu Yönetimi

Kısa cevap: birden fazla worker aynı hedef site için CAPTCHA çözüyorsa, oturum durumunu her worker'ın belleğinde değil Redis gibi paylaşılan bir depoda tutun. Çerezler, token'lar ve proxy atamaları merkezde durunca hedef site tek ve tutarlı bir oturum görür, worker'lar birbirinin çözümünü yeniden kullanır.

Bu tutarlılık, İstanbul'daki bir e-ticaret ekibinin ödeme akışını paralel worker'larla test ettiği bir QA senaryosunda hemen fark edilir: durum paylaşılmazsa her worker ayrı çerez ve ayrı IP taşır, hedef site bunu düzensiz davranış olarak okur.

Sorun: her worker kendi oturumunu taşır

Paylaşılan durum olmadan her worker sıfırdan başlar: farklı çerezler, farklı IP'ler ve farklı tarayıcı profilleri üretir, süresi dolmuş oturumlardaki çözümleri boşa harcar.

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 ❌

Üçüncü worker çürümüş bir çereze güvendiği için CAPTCHA'yı yeniden çözmek zorunda kalır; bu tekrarlar gecikmeyi ve maliyeti artırır.

Paylaşılan durumda ne saklanır?

Her bileşenin ömrü farklıdır, paylaşım stratejisi de buna göre değişir: kısa ömürlü veriler kısa TTL'li anahtarlarda, kimlik doğrulama verileri TTL'li hash'lerde durur.

Durum Bileşeni Ömür boyu Paylaşım Stratejisi
Kimlik doğrulama çerezleri Dakikalardan saatlere TTL ile Redis
CAPTCHA belirteçleri 90–300 saniye Redis listesi (kısa TTL)
qa_session_cookie çerezleri ~30 dakika Redis karma
CSRF belirteçleri Sayfa yükü başına Paylaşmayın; her çalışanın kendine ait bir hakkı var
Tarayıcı test profil yapılandırması Kalıcı Çalışma zamanı durumu değil yapılandırma
Vekil ataması Oturum başına Redis destekli proxy havuzu

CSRF token'larını asla paylaşmayın: her worker kendi token'ını almalıdır.

Mimari: Redis merkezli durum deposu

Tüm worker'lar aynı Redis örneğine bakar: çerezler domain'e, token'lar sitekey'e, proxy'ler bir havuza, kilitler domain başına anahtarlanır.

┌──────────────────────────────────────┐
│          Session State Store          │
│              (Redis)                  │
│                                      │
│  cookies:{domain} → Hash             │
│  tokens:{sitekey} → List             │
│  proxies:pool → Set                  │
│  locks:{domain}:{worker} → String    │
└─────┬──────────┬──────────┬──────────┘
      │          │          │
  ┌───▼───┐  ┌──▼────┐  ┌──▼────┐
  │Worker1│  │Worker2│  │Worker3│
  └───────┘  └───────┘  └───────┘

Paylaşılan durumu yetkili QA akışıyla sınırlayın

Paylaşılan oturum durumunu yalnızca kendi test ortamlarınızda ya da yetki verilmiş QA akışlarında kullanın. İstanbul'daki ekip örneğinde olduğu gibi, çerez ve oturum verisi kişisel veri içerebilir; Türkiye'de bu tür senaryolar KVKK yükümlülükleri kapsamındadır. Redis'te ne sakladığınızı ve ne kadar süre tuttuğunuzu bilinçli seçin:

  • Çerezleri yalnızca test süresi boyunca saklayın; kısa TTL ile otomatik silinmelerini sağlayın.
  • Gerçek kullanıcı oturumları yerine staging.example.com/qa-login gibi QA hesaplarını kullanın.
  • Redis örneğini erişim denetimiyle koruyun, herkese açık bırakmayın.

Bu üç kural, paylaşılan durumu hem güvenli hem de denetlenebilir tutar.

Python ile uygulama

Oturum deposu (SessionStore)

SessionStore çerezleri ve token'ları TTL ile saklar, ayrıca oturumu tek worker'ın yönetmesi için bir kilit sunar. save_token kısa TTL kullanır çünkü token'lar hızlı çürür.

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)

Paylaşılan durumu kullanan worker

Worker önce paylaşılan çerezleri yükler, sonra CAPTCHA'yı çözer. solve_captcha her seferinde CaptchaAI'ye gitmez: önce Redis'teki token havuzuna bakar. Elde ettiği token'ı g-recaptcha-response alanına enjekte edip formu gönderir.

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"]}

Proxy havuzu yönetimi

Tüm worker'lar aynı IP'den çıkarsa hedef site onları tek varlık sayar. ProxyPool her worker'a havuzdan boş bir proxy atar, iş bitince geri verir.

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)

Node.js ile uygulama

Aynı deseni Node.js tarafında ioredis ve axios ile kurabilirsiniz. workerSolve önce havuza bakar, boşsa CaptchaAI'ye gönderip sonucu sorgular; Turnstile ya da reCAPTCHA fark etmeksizin akış aynıdır.

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" };
}

Durum yönetimi desenleri

Desenleri birleştirebilirsiniz: kimlik doğrulamalı akışlarda oturum kilidi ve çerez paylaşımı, durumsuz yüksek verimli çözümlerde token havuzu öne çıkar.

Desen Ne Zaman Kullanılmalı
Oturum kilidi Bir çalışan oturum açma işlemini yönetir, diğerleri ise çerezleri kullanır
Jeton havuzu Yüksek verimlilik: belirteçleri önceden çözün ve dağıtın
Çerez paylaşımı Çalışanların kimliği doğrulanmış oturumlara ihtiyacı var
Vekil yakınlığı Hedef site IP oturumu bağlamayı izler

Sorun giderme

Dağıtık kurulumlarda sorunların çoğu eksik paylaşımdan ya da yanlış TTL'den doğar.

Sorun Sebep Düzeltme
Çalışanlar farklı oturumlar alıyor Redis aracılığıyla paylaşılmayan çerezler Başarılı isteklerden sonra save_cookies'nin çağrıldığını doğrulayın
Tokenın süresi, diğer çalışanlar onu kullanmadan önce doldu TTL çok uzun veya ağ gecikmesi TTL güvenlik marjını azaltın; Jetonları alındıktan sonraki 10 saniye içinde kullanın
Oturum kilidi hiç serbest kalmadı Worker çöktü Kilit anahtarındaki TTL otomatik serbest bırakır (varsayılan 300 saniye)
Hedef site çalışanları engelliyor Tüm çalışanlar aynı proxy'yi kullanıyor Çalışan başına benzeşimle proxy havuzunu kullanın

Sık sorulan sorular

CaptchaAI thread sayısı worker sayısıyla eşleşmeli mi?

Zorunlu değil ama pratik bir tavan koyar. CaptchaAI thread bazlı faturalandırır; her thread aynı anda çözülen bir CAPTCHA'dır. BASIC ($15/ay, 5 thread) ile beş çözüm akışını paralel yürütebilirsiniz; daha fazla eşzamanlı worker için ADVANCE ($90/ay, 50 thread) gibi bir plana geçin.

Birden fazla worker token havuzunu aynı anda çekerse çakışma olur mu?

Hayır. Token'lar Redis listesinde tutulur ve lpop atomiktir; iki worker aynı token'ı asla alamaz. Havuz boşsa worker CaptchaAI'ye yeni istek gönderip çözer ve sonucu listeye ekler.

Redis yerine başka bir durum deposu kullanabilir miyim?

Evet. Desen depoya bağlı değil; atomik işlemler ve TTL sunan herhangi bir paylaşımlı depo işe yarar. Redis düşük gecikmesi ve yerleşik TTL'si nedeniyle pratik seçimdir.

Bir worker çökerse paylaşılan oturuma ne olur?

Oturum kilidi TTL ile korunur (varsayılan 300 saniye), böylece çöken worker kilidi sonsuza kadar tutamaz; süre dolunca başka bir worker oturumu devralır. Paylaşılan çerezler Redis'te kaldığı için tek bir worker'ın çökmesi diğerlerini durdurmaz.

Sonraki adımlar

Dağıtık CAPTCHA worker'larınızı tutarlı oturumlarla koordine edin: CaptchaAI API anahtarınızı alın ve reCAPTCHA ile Turnstile'ı aynı akıştan çözün.

İlgili kılavuzlar:

Bu makale için yorumlar devre dışı bırakılmıştır.