Tutorial

Rate-Limited Concurrency: Token Bucket untuk CAPTCHA API Call

Konkurensi yang tidak terkontrol mengirimkan permintaan secepat mungkin. Hal ini menyebabkan ERROR_TOO_MUCH_REQUESTS, saldo API terbuang, dan biaya yang tidak dapat diprediksi. Keranjang token memungkinkan Anda menetapkan kecepatan yang tepat – “tidak lebih dari 20 kiriman per detik” – sambil tetap memungkinkan semburan singkat ketika kapasitas tersedia.

Cara Kerja Token Bucket

[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

Properti utama:

  • Kapasitas — ukuran burst maksimum
  • Tingkat isi ulang – permintaan berkelanjutan per detik
  • Permintaan menunggu ketika keranjang kosong (tidak ada penolakan, hanya pembatasan)

Implementasi Python

Bucket Token yang Aman untuk Thread

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

Pemecah CAPTCHA dengan Tarif Terbatas

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

Implementasi JavaScript

Keranjang Token Asinkron

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

Pemecah Batch dengan Tarif Terbatas

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

Memilih Parameter

Beban kerja Kapasitas (meledak) Kecepatan isi ulang (berkelanjutan)
Goresan ringan 5 2/sec
Otomatisasi standar 20 10/sec
Pipa bervolume tinggi 50 30/sec
Throughput maksimum 100 50/sec

Aturan praktis:

  • Atur kapasitas ke 2× kecepatan isi ulang (memungkinkan semburan 2 detik)
  • Mulailah secara konservatif, tingkatkan sambil memantau tingkat kesalahan
  • Hanya submit yang dibatasi tarifnya — jajak pendapat bersifat ringan dan dapat dibatasi sendiri

Token Bucket vs Algoritma Lainnya

Algoritma Perilaku Terbaik untuk
ember token Tingkat halus dengan tunjangan burst Panggilan API CAPTCHA
Ember bocor Tingkat keluaran tetap, tidak ada ledakan Persyaratan tarif yang ketat
Memperbaiki jendela Hitung per timing window, tepian meledak Penghitung sederhana
Jendela geser Hitung selama periode bergulir Penegakan tarif yang akurat

Token bucket adalah default terbaik – ini memungkinkan lonjakan alami (scraper menemukan 20 CAPTCHA sekaligus) sambil menerapkan tarif berkelanjutan.

Pemecahan masalah

Masalah Sebab Perbaiki
Permintaan masih dibatasi Pembatas tarif disetel lebih tinggi dari yang diizinkan API Tingkat isi ulang yang lebih rendah agar sesuai dengan batas CaptchaAI
Latensi tinggi berdasarkan permintaan Token habis, menunggu diisi ulang Tingkatkan kapasitas untuk skenario burst
Memori bertambah Tunggu antrian terakumulasi Tetapkan ukuran antrian maksimum; menolak permintaan berlebih
Pembatas nilai tidak dibagikan ke seluruh proses Hanya dalam memori Gunakan keranjang token berbasis Redis untuk pembatasan tarif terdistribusi

Pertanyaan Umum

Haruskah saya membatasi submit, jajak pendapat, atau keduanya?

submit dengan batas tarif saja. Permintaan pemungutan suara ringan dan dapat dilakukan sendiri melalui time.sleep(5). Pemungutan suara yang terlalu dibatasi akan meningkatkan penyelesaian latensi tanpa manfaat.

Bagaimana cara menangani ERROR_TOO_MUCH_REQUESTS meskipun ada batasan tarif?

Batas tarif Anda ditetapkan terlalu tinggi. Turunkan tingkat isi ulang. Periksa juga apakah beberapa proses berbagi kunci API yang sama – tingkat agregat di semua proses.

Bisakah saya menggunakan pembatas tarif per jenis CAPTCHA?

Ya – buat keranjang token terpisah untuk jenis CAPTCHA yang berbeda. Hal ini mencegah tugas reCAPTCHA v2 bervolume tinggi membuat submit Turnstile kelaparan.

Artikel Terkait

  • Panggilan Captcha Api Pola circuit breaker

Langkah Selanjutnya

Membangun penyelesaian CAPTCHA dengan tingkat pengendalian —dapatkan kunci API CaptchaAI Andadan menerapkan tingkat permintaan yang berkelanjutan.

Panduan terkait:

Komentar dinonaktifkan untuk artikel ini.