Tutorials

Token bucket ile CAPTCHA API çağrılarında hız sınırlama

Hız sınırını tek bir yere koyun: her istek in.php'ye gitmeden önce bir token bucket'tan (token kovası) izin alsın. Bu tek katman, ERROR_TOO_MUCH_REQUESTS hatalarını, boşa harcanan bakiyeyi ve tahmin edilemeyen gecikmeyi aynı anda ortadan kaldırır.

Sorunun kaynağı genellikle eşzamanlılık ayarının yanlış yerde olmasıdır. ThreadPoolExecutor(max_workers=30) yazdığınızda 30 isteği düzenli aralıklarla değil, art arda ve olabildiğince hızlı gönderirsiniz; çözüm süreleri değiştikçe gerçek gönderim hızı saniyede 3 ile 40 arasında salınır. CaptchaAI planları eşzamanlı thread üzerinden faturalandığı için — BASIC ($15/ay, 5 thread), ADVANCE ($90/ay, 50 thread) — asıl kontrol etmeniz gereken şey toplam istek sayısı değil, birim zamandaki gönderim yoğunluğudur. Token bucket tam olarak bunu yapar: sürekli bir oran uygular, ama kapasite varken kısa patlamalara da izin verir.

Hız sınırını gönderim katmanına koymanın karşılığı

  • Hata yerine bekleme. Sınırı kendi tarafınızda uyguladığınızda istek reddedilmez, sadece birkaç yüz milisaniye bekler. Hata yönetimi kodunuz sadeleşir.
  • Öngörülebilir maliyet. Türkiye'deki ekiplerin çoğu bu servisleri USD ile öder; kur oynaklığı yüzünden aylık sabit, thread tabanlı bir plan çözüm başına ücretlendirmeye göre çok daha kolay bütçelenir. Bunun ön koşulu, planınızın thread kapasitesini gerçekten doldurabilen düzgün bir gönderim akışıdır.
  • Doygunluğun görünür olması. Sabit bir oranla gönderdiğinizde, gecikme tırmandığında bunun sizin patlamanızdan mı yoksa çözüm tarafındaki yükten mi kaynaklandığını ayırt edebilirsiniz.

Token bucket nasıl çalışır?

Kova sabit bir hızda token'la dolar, her istek çıkarken bir token harcar. Kova doluysa istekler hemen geçer; boşsa sıradaki istek bir sonraki dolum turunu bekler.

[Bucket] capacity=20, refill=10/sec

Time 0:  ████████████████████  20 tokens available
         → 15 requests consume 15 tokens
Time 0:  █████                 5 tokens remain

Time 1s: ███████████████       15 tokens (5 + 10 refilled)
         → 15 requests consume 15 tokens
Time 1s: (empty)               0 tokens

Time 2s: ██████████            10 tokens (0 + 10 refilled)
         → Request waits if bucket is empty

Üç parametre her şeyi belirler:

  • Kapasite — tek seferde izin verilen en büyük patlama boyutu
  • Yeniden dolum oranı — saniye başına sürdürülebilir istek sayısı
  • Bekleme davranışı — kova boşken istek reddedilmez, kuyrukta bekler

Token bucket, sızdıran kova ve pencere sayaçları

Algoritma Davranış Nerede işe yarar
Token bucket Sürekli oran + patlama payı CAPTCHA API gönderimleri
Sızdıran kova Sabit çıkış hızı, patlama yok Katı oran şartı olan entegrasyonlar
Sabit pencere Pencere başına sayaç, kenar patlamaları Basit sayaç ihtiyaçları
Kayan pencere Süregelen zaman aralığında sayım Hassas oran denetimi

CAPTCHA iş yükleri için mantıklı varsayılan token bucket'tır. Veri kazıma akışları düz bir hızda ilerlemez: tarayıcı botunuz tek bir sayfada 20 CAPTCHA doğrulaması bulur, sonra dakikalarca hiçbir şey bulmaz. Sızdıran kova bu patlamayı yapay olarak yayar ve toplam süreyi uzatır; token bucket ise patlamayı kapasite kadar geçirip ortalamayı korur.

Python tarafı: kova ve hız sınırlı çözüm akışı

Thread güvenli token bucket

acquire() çağrısı token bulana kadar bloklar, timeout verildiğinde ise False döner. Kilit, birden fazla worker aynı anda token istediğinde sayacın bozulmasını engeller.

import time
import threading


class TokenBucket:
    def __init__(self, capacity, refill_rate):
        """
        Args:
            capacity: Maximum tokens (burst size)
            refill_rate: Tokens added per second
        """
        self.capacity = capacity
        self.refill_rate = refill_rate
        self.tokens = capacity
        self.last_refill = time.monotonic()
        self.lock = threading.Lock()

    def acquire(self, timeout=None):
        """Block until a token is available."""
        deadline = time.monotonic() + timeout if timeout else float("inf")

        while True:
            with self.lock:
                self._refill()
                if self.tokens >= 1:
                    self.tokens -= 1
                    return True

            # Check timeout
            if time.monotonic() >= deadline:
                return False

            # Wait before retrying (avoid busy loop)
            time.sleep(min(1.0 / self.refill_rate, 0.1))

    def _refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        new_tokens = elapsed * self.refill_rate
        self.tokens = min(self.capacity, self.tokens + new_tokens)
        self.last_refill = now

Gönderimi kovaya bağlayan çözüm fonksiyonu

Kovayı yalnızca in.php'ye giden gönderimin önüne koyun. Sonuç sorgulaması ayrı bir konudur: res.php istekleri hafiftir ve time.sleep(5) ile zaten kendi kendini sınırlar.

import os
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Allow 10 submissions/sec with burst of 20
rate_limiter = TokenBucket(capacity=20, refill_rate=10)


def solve_captcha_rate_limited(sitekey, pageurl):
    """Solve with rate limiting on submission."""
    # Wait for token before submitting
    rate_limiter.acquire()

    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:
        raise RuntimeError(data.get("request"))

    captcha_id = data["request"]

    # Polling doesn't need rate limiting (separate concern)
    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:
            return result["request"]
        if result.get("request") != "CAPCHA_NOT_READY":
            raise RuntimeError(result.get("request"))

    raise TimeoutError("Solve timeout")


# Run 100 tasks through rate limiter
tasks = [
    {"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
     "pageurl": f"https://example.com/p/{i}"}
    for i in range(100)
]

with ThreadPoolExecutor(max_workers=30) as executor:
    futures = {
        executor.submit(
            solve_captcha_rate_limited, t["sitekey"], t["pageurl"]
        ): t for t in tasks
    }

    for future in as_completed(futures):
        task = futures[future]
        try:
            solution = future.result()
            print(f"[OK] {task['pageurl']}")
        except Exception as e:
            print(f"[ERR] {task['pageurl']}: {e}")

Node.js tarafı: asenkron kova ve toplu gönderim

Promise tabanlı token bucket

Node.js'te bloklama yerine bekleme süresini hesaplayıp setTimeout ile beklersiniz; olay döngüsü bu sırada diğer işleri yürütmeye devam eder.

class TokenBucket {
  constructor(capacity, refillRate) {
    this.capacity = capacity;
    this.refillRate = refillRate; // tokens per second
    this.tokens = capacity;
    this.lastRefill = Date.now();
    this.waitQueue = [];
  }

  _refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
    this.lastRefill = now;
  }

  async acquire() {
    this._refill();

    if (this.tokens >= 1) {
      this.tokens -= 1;
      return;
    }

    // Wait until a token is available
    const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
    await new Promise((resolve) => setTimeout(resolve, waitTime));

    this._refill();
    this.tokens -= 1;
  }
}

100 görevlik toplu koşuda hız sınırı

Promise.allSettled yüz görevi aynı anda başlatır, ama her biri kovadan izin almadan istek gönderemez. Sonuçta eşzamanlılık yüksek, gönderim hızı sabit kalır.

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const rateLimiter = new TokenBucket(20, 10); // 20 burst, 10/sec sustained

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function solveCaptchaLimited(sitekey, pageurl) {
  // Wait for rate limit token
  await rateLimiter.acquire();

  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;

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

// Solve 100 tasks — rate limiter ensures max 10 submissions/sec
async function batchSolve(tasks) {
  const results = await Promise.allSettled(
    tasks.map((t) => solveCaptchaLimited(t.sitekey, t.pageurl))
  );

  const solved = results.filter((r) => r.status === "fulfilled").length;
  const failed = results.filter((r) => r.status === "rejected").length;
  console.log(`Solved: ${solved}, Failed: ${failed}`);
}

Kapasite ve yeniden dolum oranını seçme

İş yükü Kapasite (patlama) Yeniden dolum oranı (sürekli)
Hafif veri kazıma 5 2/sn
Standart otomasyon 20 10/sn
Yüksek hacimli akış 50 30/sn
Azami verim 100 50/sn

Tablodaki değerler başlangıç noktasıdır; üst sınırı planınızın thread sayısı belirler. Kaba hesap şudur:

sürdürülebilir gönderim oranı ≈ thread sayısı ÷ o CAPTCHA türünün çözüm süresi

CaptchaAI'nin yayınladığı tavan sürelerle bakarsak: Cloudflare Turnstile 10 saniyenin altında tamamlanır, yani 50 thread'li ADVANCE planında saniyede yaklaşık 5 gönderim sürdürülebilir bir orandır. reCAPTCHA v2 için tavan 60 saniyedir; aynı thread sayısı burada çok daha düşük bir sürekli orana karşılık gelir. Kovayı bu üst sınırın üzerine ayarlarsanız kuyruk çözüm tarafında değil, sizin sürecinizin belleğinde birikir.

Pratik kurallar:

  • Kapasiteyi yeniden dolum oranının iki katı tutun; bu, iki saniyelik patlamalara izin verir.
  • Temkinli başlayın, hata oranını izleyerek kademeli artırın.
  • Yalnızca gönderimleri sınırlayın; sorgulama isteklerini sınırlamak çözüm süresini uzatır, hiçbir şey kazandırmaz.

Örnek senaryo: gece çalışan bir e-ticaret QA paketi

İstanbul'da çalışan bir ekip, kendi ödeme akışının staging kopyasında her gece 03:00'te (Europe/Istanbul) regresyon testi koşuyor: yaklaşık 4.000 senaryo, her birinin girişinde bir CAPTCHA doğrulaması. Koşu tek bir pencereye sıkıştığı için istekler ilk dakikada patlama yapıyor, ardından uzun süre boşta kalıyordu.

Yapılan tek değişiklik, gönderim fonksiyonunun önüne TokenBucket(capacity=20, refill_rate=10) eklemek oldu. Toplam süre neredeyse aynı kaldı, çünkü darboğaz zaten çözüm süresiydi; buna karşılık ERROR_TOO_MUCH_REQUESTS kaynaklı yeniden denemeler ortadan kalktı ve koşu süresi geceden geceye tahmin edilebilir hale geldi.

İki not: test verinizde gerçek müşteri bilgisi varsa bu veri KVKK kapsamındadır — üretim kopyası yerine maskelenmiş veri kullanın. Ayrıca cron ifadenizi UTC değil Europe/Istanbul saat dilimine sabitleyin; yaz saati kayması yüzünden koşunun trafik yoğun saate düşmesi, hız sınırından çok daha büyük bir sorundur.

Sorun giderme

Belirti Olası neden Çözüm
Hız sınırına rağmen ERROR_TOO_MUCH_REQUESTS Yeniden dolum oranı planın taşıyabileceğinin üstünde Oranı kademeli düşürün, hata oranını izleyin
Gönderim gecikmesi sürekli tırmanıyor Kova boş, istekler token bekliyor Kapasiteyi artırın ya da patlamayı kaynağında dağıtın
Süreç belleği şişiyor Bekleme kuyruğu sınırsız büyüyor Azami kuyruk boyutu koyun, fazla isteği reddedip yeniden deneyin
Her worker kendi sınırını uyguluyor Kova yalnızca süreç belleğinde Ortak sayaç için Redis tabanlı bir kova kullanın
Hata yok ama verim düşük Sınır sizde değil, thread kapasitesinde Oranı değil, thread sayısını artırın (üst plana geçin)

Sık sorulan sorular

Yeniden dolum oranını planımdaki thread sayısına göre nasıl seçerim?

Üst sınır olarak thread sayısını beklenen çözüm süresine bölün. 50 thread'li ADVANCE planında ve 10 saniyenin altında tamamlanan Cloudflare Turnstile gibi bir türde bu, saniyede yaklaşık 5 gönderim eder. Daha yavaş türlerde aynı thread sayısı çok daha düşük bir sürekli orana karşılık gelir.

Aynı API anahtarını birden fazla worker kullanıyorsa ne yapmalıyım?

Bellek içi kova her süreçte ayrı çalışır: üç worker'ın her biri saniyede 10 gönderirse anahtarınız saniyede 30 istek görür. Ortak bir sayaç gerekir — token'ları Redis'te tutan bir kova ya da tüm gönderimleri tek bir kuyruktan geçiren küçük bir servis.

Token bucket, devre kesici (circuit breaker) ile aynı işi mi yapar?

Hayır, ikisi farklı katmanlardır. Token bucket normal koşullarda hızı düzenler; devre kesici arka arkaya hata geldiğinde akışı tamamen durdurur. Birlikte kullanıldığında kova ortalamayı korur, devre kesici arıza anında boşa giden istekleri keser.

Kova boşken istekleri beklemek yerine reddetmek daha mı iyi?

Toplu işlerde bekleme doğru davranıştır: iş zaten arka planda çalışıyordur ve gecikme kimseyi rahatsız etmez. Kullanıcının ekran başında beklediği bir akışta ise kuyruğa azami boyut koyup fazlasını hemen reddetmek, herkesin zaman aşımına düşmesinden iyidir.

İlgili makaleler

Sonraki adım

Hız kontrolünü bugün ekleyin: CaptchaAI API anahtarınızı alın ve ilk toplu koşunuzu sabit bir gönderim oranıyla çalıştırın.

Devamı için:

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