Tutorials

Ratenbegrenzte Parallelität: Token-Bucket für CAPTCHA-API-Aufrufe

Durch unkontrollierte Parallelität werden Anfragen so schnell wie möglich gesendet. Das führt zu ERROR_TOO_MUCH_REQUESTS, verschwendetem API-Guthaben und unvorhersehbaren Kosten. Mit einem Token-Bucket können Sie eine genaue Rate festlegen – „nicht mehr als 20 Übermittlungen pro Sekunde“ – und gleichzeitig kurze Bursts zulassen, wenn Kapazität verfügbar ist.

So funktioniert ein 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

Haupteigenschaften:

  • Kapazität – maximale Burst-Größe
  • Auffüllungsrate – anhaltende Anfragen pro Sekunde
  • Anfragen warten, wenn der Bucket leer ist (keine Ablehnung, nur Drosselung)

Python-Implementierung

Thread-sicherer Token-Bucket

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

Ratenbegrenzter CAPTCHA-Löser

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

JavaScript-Implementierung

Asynchroner Token-Bucket

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

Ratenbegrenzter Stapellöser

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

Parameter auswählen

Arbeitsbelastung Kapazität (Burst) Nachfüllrate (anhaltend)
Leichtes Schaben 5 2/sec
Standardautomatisierung 20 10/sec
Großvolumige Pipeline 50 30/sec
Maximaler Durchsatz 100 50/sec

Faustregeln:

  • Stellen Sie die Kapazität auf eine Nachfüllrate von 2 × ein (ermöglicht 2-Sekunden-Bursts).
  • Beginnen Sie konservativ, erhöhen Sie die Fehlerquote und überwachen Sie sie gleichzeitig
  • Nur Übermittlungen mit Ratenbegrenzung – die Abfrage ist leichtgewichtig und selbstlimitierend

Token Bucket im Vergleich zu anderen Algorithmen

Algorithmus Verhalten Am besten für
Token-Eimer Glatter Tarif mit Burst-Zulage CAPTCHA-API-Aufrufe
Undichter Eimer Feste Ausgaberate, keine Bursts Strenge Tarifanforderungen
Festes Fenster Anzahl pro Zeitfenster, Kantenausbrüche Einfache Zähler
Schiebefenster Zählen Sie über den rollierenden Zeitraum Präzise Tarifdurchsetzung

Der Token-Bucket ist die beste Standardeinstellung – er ermöglicht natürliche Bursts (der Scraper findet 20 CAPTCHAs auf einmal) und erzwingt gleichzeitig eine dauerhafte Rate.

Fehlerbehebung

Problem Ursache Lösung
Token wird erzeugt, aber vom Ziel abgelehnt sitekey, pageurl oder Session-Kontext stimmen nicht Erfasse Parameter erneut und verwende den Token in derselben Browser- oder HTTP-Sitzung
Polling endet im Timeout Intervall, Wartezeit oder Fehlerbehandlung sind zu eng gesetzt Poll alle 5-10 Sekunden, trenne Timeout von echten Fehlercodes und logge die Ursache
Beispiel funktioniert lokal, aber nicht im Workflow Callback, Form-Feld oder Token-Injektion fehlt in der echten Zielkette Prüfe den exakten Übergabepfad vom Solver bis zur finalen Zielanfrage

Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.