Findet Ihr Scraper 100 CAPTCHAs auf einmal und schickt alle gleichzeitig an die API, antwortet CaptchaAI mit ERROR_TOO_MUCH_REQUESTS – Guthaben verbrannt, Durchsatz eingebrochen. Die saubere Lösung ist ein Token-Bucket: Er begrenzt Ihre Übermittlungen auf eine feste Rate wie „höchstens 20 pro Sekunde“ und lässt trotzdem kurze Bursts zu, solange freie Kapazität im Bucket liegt. Dieser Leitfaden zeigt eine Thread-sichere Implementierung in Python und eine asynchrone Variante in JavaScript – jeweils direkt vor der Übermittlung an die CaptchaAI-API eingehängt.
So funktioniert ein Token-Bucket
Ein Token-Bucket ist ein Behälter mit fester Kapazität, der sich kontinuierlich mit Tokens auffüllt. Jede Anfrage entnimmt ein Token; ist der Bucket leer, wartet die Anfrage, bis wieder aufgefüllt wurde. Kein Token geht verloren, keine Anfrage wird abgelehnt – der Verkehr wird lediglich geglättet.
[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
Die drei Stellschrauben:
- Kapazität – die maximale Burst-Größe, also wie viele Anfragen kurzzeitig auf einmal durchgehen dürfen.
- Nachfüllrate – die dauerhaft mögliche Anzahl Anfragen pro Sekunde.
- Bei leerem Bucket warten Anfragen (keine Ablehnung, nur Drosselung).
Token-Bucket im Vergleich zu anderen Verfahren
Bevor Sie implementieren, lohnt der Blick auf die Alternativen. Vier Rate-Limiting-Verfahren sind gebräuchlich, und sie verhalten sich bei Bursts unterschiedlich.
| Verfahren | Verhalten | Am besten geeignet für |
|---|---|---|
| Token-Bucket | Gleichmäßige Rate mit Burst-Spielraum | CAPTCHA-API-Aufrufe |
| Leaky Bucket | Feste Ausgaberate, keine Bursts | Strenge Ratenvorgaben |
| Fixed Window | Zähler pro Zeitfenster, Bursts an den Rändern | Einfache Zähler |
| Sliding Window | Zählung über ein gleitendes Intervall | Präzise Ratendurchsetzung |
Für CAPTCHA-API-Aufrufe ist der Token-Bucket die beste Standardwahl: Er erlaubt natürliche Bursts – etwa wenn der Scraper 20 CAPTCHAs auf einmal findet – und erzwingt dennoch eine dauerhaft stabile Rate.
Python-Implementierung
Der Kern ist eine Klasse, die Tokens zeitbasiert nachfüllt und den Zugriff über ein Lock absichert, damit mehrere Threads sie gefahrlos teilen.
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
Der Limiter wird genau einmal instanziiert und von allen Worker-Threads geteilt. acquire() steht vor jeder Übermittlung – das Polling bleibt bewusst außen vor, weil es sich über sein Warteintervall selbst begrenzt.
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
In Node.js läuft alles in einer Event-Loop, ein Lock entfällt daher. Statt zu blockieren, wartet acquire() per await, bis genug Tokens nachgefüllt sind.
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
Alle Aufgaben starten über Promise.allSettled scheinbar gleichzeitig – der geteilte rateLimiter sorgt dafür, dass die Übermittlungen dennoch mit höchstens 10 pro Sekunde durchsickern.
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}`);
}
Kapazität und Nachfüllrate wählen
Die passenden Werte hängen vom Durchsatz Ihrer Pipeline ab. Diese Richtwerte sind ein guter Ausgangspunkt:
Richtwerte nach Arbeitslast
| Arbeitslast | Kapazität (Burst) | Nachfüllrate (dauerhaft) |
|---|---|---|
| Leichtes Scraping | 5 | 2/s |
| Standard-Automatisierung | 20 | 10/s |
| Große Pipeline | 50 | 30/s |
| Maximaler Durchsatz | 100 | 50/s |
Faustregeln für die Wahl
- Setzen Sie die Kapazität auf das Doppelte der Nachfüllrate – das erlaubt Bursts von rund zwei Sekunden.
- Beginnen Sie konservativ und erhöhen Sie die Rate schrittweise, während Sie die Fehlerquote beobachten.
- Drosseln Sie nur die Übermittlungen – das Polling ist leichtgewichtig und begrenzt sich über sein Warteintervall von selbst.
Rate-Limiting über mehrere Worker verteilen
In der Praxis läuft eine Scraping-Pipeline selten auf einem einzigen Rechner. Verteilen Sie die Last etwa auf mehrere Hetzner- oder netcup-VPS, hält jeder Prozess seinen eigenen In-Memory-Bucket – und die Summe der lokalen Raten übersteigt schnell das, was ein einzelner API-Schlüssel verträgt. Zwei Ansätze helfen:
- Rate pro Prozess aufteilen: Bei fünf Workern und einem Ziel von 10 Übermittlungen pro Sekunde bekommt jeder Worker eine Nachfüllrate von 2/s.
- Gemeinsamer Bucket über Redis: Ein zentraler Token-Bucket in Redis erzwingt die Rate prozessübergreifend – der robustere Weg, sobald Worker dynamisch hoch- und herunterskalieren.
Hinweis: Orientieren Sie die Zielrate an Ihrem Thread-Kontingent. CaptchaAI rechnet pro gleichzeitigem Thread ab – nicht pro Lösung. Der Tarif ADVANCE (90 $/Monat, 50 Threads) erlaubt deutlich aggressivere Raten als BASIC (15 $/Monat, 5 Threads). Es bringt nichts, mehr Anfragen pro Sekunde zu übermitteln, als Ihre Threads gleichzeitig lösen können.
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
| Anfragen werden trotz Limiter gedrosselt | Rate höher gesetzt als der API-Schlüssel zulässt | Nachfüllrate senken, bis sie zu den Limits von CaptchaAI passt |
| Hohe Latenz pro Anfrage | Tokens erschöpft, Anfragen warten auf Nachfüllung | Kapazität erhöhen, um Bursts abzufedern |
| Speicherverbrauch wächst | Die Warteschlange der wartenden Anfragen läuft voll | Maximale Warteschlangengröße setzen und Überschuss abweisen |
| Limiter greift nicht über Prozesse hinweg | Bucket liegt nur im Arbeitsspeicher | Redis-basierten Token-Bucket für verteiltes Rate-Limiting nutzen |
Häufige Fragen
Sollte ich Übermittlungen, Polling oder beides drosseln?
Nur die Übermittlungen. Polling-Anfragen sind leichtgewichtig und drosseln sich über time.sleep(5) bereits selbst. Ein zusätzliches Limit auf dem Polling erhöht nur die Lösungszeit, ohne etwas zu bringen.
Wie hängt die Nachfüllrate mit meinem Thread-Kontingent zusammen?
Direkt. CaptchaAI rechnet pro gleichzeitigem Thread ab, und mehr Threads erlauben eine höhere dauerhafte Rate. Eine Übermittlungsrate, die dauerhaft über der Zahl gleichzeitig lösbarer Threads liegt, füllt nur die Warteschlange – sie beschleunigt nichts.
Funktioniert ein In-Memory-Bucket über mehrere Worker hinweg?
Nein. Jeder Prozess hält seinen eigenen Zähler, sodass sich die Gesamtrate mit der Zahl der Worker multipliziert. Teilen Sie die Zielrate auf die Prozesse auf oder verwenden Sie einen gemeinsamen Bucket in Redis.
Warum lehnt der Token-Bucket Anfragen nicht ab, sondern lässt sie warten?
Das ist der Kern des Prinzips: Bei leerem Bucket wartet die Anfrage auf das nächste Token, statt einen Fehler zu werfen. So bleibt der Durchsatz stabil, und Sie brauchen keine Retry-Logik für abgewiesene Anfragen.