Ein Circuit Breaker ist die Sicherung vor Ihrem API-Client: Häufen sich Fehler, unterbricht er die Aufrufe für eine festgelegte Zeit und lässt danach genau eine Testanfrage durch. Für CAPTCHA-Pipelines heißt das konkret, dass nicht die komplette Warteschlange in denselben Timeout läuft und kein Worker minutenlang gegen einen gestörten Endpunkt arbeitet.
Der wirtschaftliche Effekt liegt dabei woanders: CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung. Ein fehlgeschlagener Aufruf kostet also keine zusätzliche Gebühr – er belegt aber einen Ihrer Threads, bis das Timeout greift, und blockiert Kapazität, für die Sie bereits bezahlt haben.
Wann ein Circuit Breaker mehr bringt als reine Wiederholungsversuche
Wiederholungslogik behandelt den Einzelfall: Ein Aufruf scheitert, der nächste gelingt. Liegt die Störung aber auf der Gegenseite – etwa ERROR_NO_SLOT_AVAILABLE, weil alle Threads Ihres Kontos belegt sind –, verschlimmert jeder weitere Versuch die Lage: Zehn Worker mit je drei Wiederholungen erzeugen 30 Anfragen, die alle scheitern und jeweils bis zu 15 s Timeout kosten.
Der Circuit Breaker arbeitet eine Ebene höher. Er bewertet das Fehlerbild über alle Aufrufe hinweg und entscheidet für den gesamten Prozess: keine weiteren Anfragen, bis sich die Lage messbar geändert hat. Typische Auslöser sind ein falsch ausgerollter API-Schlüssel, ein gestörter Netzwerkpfad oder ein paralleler Job, der dieselben Zugangsdaten belegt.
Die drei Zustände: closed, open und half-open
closed– Normalbetrieb. Anfragen gehen durch, Fehler werden gezählt, ein Erfolg setzt den Zähler zurück.open– Der Schwellenwert ist überschritten, Anfragen werden sofort abgelehnt. Das ist der Fail-Fast-Teil: Antwort in Millisekunden statt nach 15 s Timeout.half-open– Nach der Abklingzeit läuft genau eine Testanfrage. Gelingt sie, wechselt der Breaker nachclosed; scheitert sie, beginnt die Abklingzeit von vorn.
Der interessante Zustand ist half-open: Er ist der einzige Weg zurück in den Normalbetrieb und darf nur eine Anfrage durchlassen. Schicken mehrere Threads gleichzeitig ihre Testanfrage los, trifft der komplette Ansturm erneut auf eine womöglich noch gestörte API – genau das verhindert der Lock in der Implementierung weiter unten.
Schwellenwerte festlegen, bevor Sie implementieren
| Parameter | Geringes Volumen (< 10 Anfragen/min) | Hohes Volumen (> 100 Anfragen/min) |
|---|---|---|
failure_threshold |
3 | 10 |
recovery_timeout |
30 s | 60 s |
Der Schwellenwert muss hoch genug liegen, damit ein einzelner Timeout den Circuit nicht auslöst, und niedrig genug, damit eine gestörte API nicht weiter belastet wird – als Faustregel unter der Zahl Ihrer parallelen Worker.
Ein Beispiel aus der Praxis: Ein Berliner Team betreibt seine Scraping-Worker auf zwei Hetzner-VPS-Instanzen und startet die Jobs über GitLab CI. Gebucht ist ADVANCE (90 $/Monat, 50 Threads), 20 Worker laufen parallel. Mit failure_threshold = 3 öffnet der Circuit schon bei drei unglücklich verteilten Timeouts und bremst alle 20 Worker aus; mit 10 reagiert er auf ein echtes Störungsmuster. Die Abklingzeit bleibt bei 60 s – kurz genug, dass der CI-Lauf nicht in sein eigenes Job-Timeout rutscht.
Circuit Breaker in Python implementieren
Die folgende Klasse kapselt die drei Zustände und schützt jeden Zustandswechsel mit threading.Lock, bleibt also im Thread-Pool nutzbar. solve_captcha() übermittelt eine reCAPTCHA-v2-Aufgabe an in.php und fragt das Ergebnis über res.php ab; Fehler und Timeouts kommen als Exception zurück.
import time
import threading
import requests
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
API_KEY = "YOUR_API_KEY"
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = 0
self.state = "closed" # closed, open, half-open
self._lock = threading.Lock()
def call(self, func, *args, **kwargs):
with self._lock:
if self.state == "open":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "half-open"
print("[circuit] State: half-open — testing one request")
else:
remaining = self.recovery_timeout - (
time.time() - self.last_failure_time
)
raise CircuitOpenError(
f"Circuit open — retry in {remaining:.0f}s"
)
try:
result = func(*args, **kwargs)
with self._lock:
self.failure_count = 0
if self.state == "half-open":
print("[circuit] State: closed — API recovered")
self.state = "closed"
return result
except Exception as e:
with self._lock:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
print(
f"[circuit] State: open — "
f"{self.failure_count} failures"
)
raise
class CircuitOpenError(Exception):
pass
def solve_captcha(sitekey, page_url):
resp = requests.post(SUBMIT_URL, data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}, timeout=15)
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit error: {data['request']}")
task_id = data["request"]
for _ in range(24):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": "1",
}, timeout=15).json()
if poll["status"] == 1:
return poll["request"]
if poll["request"] != "CAPCHA_NOT_READY":
raise Exception(f"Poll error: {poll['request']}")
raise TimeoutError(f"Task {task_id} timed out")
# Usage
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30)
for i in range(10):
try:
token = breaker.call(
solve_captcha, "6Le-SITEKEY", "https://example.com"
)
print(f"[task-{i}] Solved: {token[:40]}...")
except CircuitOpenError as e:
print(f"[task-{i}] Skipped: {e}")
except Exception as e:
print(f"[task-{i}] Failed: {e}")
Ein Lauf mit Störung sieht im Log so aus:
[task-0] Solved: 03AGdBq26ZfPxL...
[task-1] Solved: 03AGdBq27AbCdE...
[task-2] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-3] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-4] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[circuit] State: open — 3 failures
[task-5] Skipped: Circuit open — retry in 28s
[task-6] Skipped: Circuit open — retry in 25s
...
[circuit] State: half-open — testing one request
[task-8] Solved: 03AGdBq28FgHiJ...
[circuit] State: closed — API recovered
Drei Fehler öffnen den Circuit, die folgenden Aufgaben werden ohne Netzwerkaufruf übersprungen, und nach 30 s prüft eine einzige Anfrage, ob die API wieder antwortet.
Dieselbe Logik in JavaScript
In Node.js entfällt der Lock, weil der Event Loop die Zustandswechsel ohnehin serialisiert. Sonst ist die Logik identisch.
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 60000;
this.failureCount = 0;
this.lastFailureTime = 0;
this.state = 'closed';
}
async call(fn, ...args) {
if (this.state === 'open') {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = 'half-open';
console.log('[circuit] State: half-open');
} else {
const remaining = this.recoveryTimeout - (Date.now() - this.lastFailureTime);
throw new Error(`Circuit open — retry in ${Math.ceil(remaining / 1000)}s`);
}
}
try {
const result = await fn(...args);
this.failureCount = 0;
if (this.state === 'half-open') {
console.log('[circuit] State: closed — recovered');
}
this.state = 'closed';
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.log(`[circuit] State: open — ${this.failureCount} failures`);
}
throw error;
}
}
}
// Usage
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const breaker = new CircuitBreaker({ failureThreshold: 3, recoveryTimeout: 30000 });
async function solveCaptcha(sitekey, pageurl) {
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) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 24; 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: taskId, json: 1 }
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
}
throw new Error('Timeout');
}
(async () => {
for (let i = 0; i < 10; i++) {
try {
const token = await breaker.call(solveCaptcha, '6Le-SITEKEY', 'https://example.com');
console.log(`[task-${i}] Solved: ${token.substring(0, 40)}...`);
} catch (err) {
console.log(`[task-${i}] ${err.message}`);
}
}
})();
Wichtig ist die Reihenfolge: Der Zustand wird geprüft, bevor axios überhaupt aufgerufen wird. Ein Breaker, der erst nach der Anfrage entscheidet, spart weder Zeit noch Thread-Kapazität.
Wiederholungslogik korrekt verschachteln
Wiederholungsversuche gehören innerhalb des Circuit Breakers, nicht darum herum: Der Breaker soll endgültige Fehler zählen, also solche, die auch nach dem letzten Versuch bestehen bleiben. Bei umgekehrter Reihenfolge steigt sein Zähler mehrfach für ein und dieselbe Ursache.
def solve_with_retry(sitekey, page_url, max_retries=2):
for attempt in range(max_retries + 1):
try:
return solve_captcha(sitekey, page_url)
except Exception:
if attempt == max_retries:
raise
time.sleep(2 ** attempt)
# Circuit breaker wraps the retry function
token = breaker.call(solve_with_retry, "6Le-SITEKEY", "https://example.com")
Das exponentielle Backoff (2 ** attempt) erzeugt Pausen von 1 s und 2 s zwischen den Versuchen. Erst wenn auch der letzte Versuch scheitert, sieht der Circuit Breaker einen Fehler.
Getrennte Circuit Breaker für Übermittlung und Polling
in.php und res.php fallen selten gemeinsam aus. Die Übermittlung kann ERROR_NO_SLOT_AVAILABLE liefern, während laufende Aufgaben weiterhin sauber abgefragt werden – ein gemeinsamer Breaker würde dann auch das Polling blockieren und halb fertige Lösungen verwerfen. Ab einigen tausend Aufgaben pro Tag lohnt sich je ein Breaker pro Endpunkt.
Fallback-Strategien für den offenen Zustand
Ein offener Circuit ist kein Abbruch, sondern eine Verzweigung:
- Zurück in die Warteschlange: die richtige Wahl für Batch-Jobs ohne harte Frist.
- Reduzierter Betrieb: Der Workflow überspringt die geschützte Quelle und arbeitet mit den übrigen weiter.
- Signal nach außen: Interaktive Anwendungen zeigen eine klare Meldung statt eines hängenden Ladebalkens.
Welche Variante wann trägt, beschreibt Fallback-Strategien bei fehlgeschlagener CAPTCHA-Lösung.
Zustandswechsel protokollieren und überwachen
Ein Circuit Breaker ohne Telemetrie ist eine Blackbox: Die Pipeline wird langsamer, und niemand weiß, warum. Protokollieren Sie jeden Zustandswechsel mit Zeitstempel, Fehleranzahl und auslösendem Fehlercode. Die aussagekräftigste Kennzahl ist die Verweildauer im Zustand open – sie ist die Zeit, in der Ihre Pipeline stillstand.
Ebenso wichtig: welche Fehler zählen dürfen. Ein ungültiger Sitekey ist ein Problem Ihres Aufrufs, kein Ausfall der API – sonst öffnet der Circuit wegen eines Konfigurationsfehlers. Welcher Code wofür steht, klärt die Übersicht der API-Antwortformate und Fehlercodes.
Typische Betriebsfehler und ihre Ursachen
| Symptom | Ursache | Lösung |
|---|---|---|
| Der Circuit öffnet zu früh | Schwellenwert zu niedrig für die Zahl paralleler Worker | failure_threshold erhöhen |
| Der Circuit schließt nie wieder | recovery_timeout zu lang |
Auf 30–60 s reduzieren, Ursache im Log prüfen |
| Race Condition im Multithread-Betrieb | Zustandswechsel nicht gesperrt | threading.Lock oder atomare Operationen |
| Bei einem Teilausfall blockiert alles | Ein Breaker für alle Endpunkte | Getrennte Breaker je Endpunkt |
| Jeder Prozess kennt einen anderen Zustand | Breaker nur im Prozessspeicher | Zähler in Redis teilen |
Häufige Fragen
Worin unterscheidet sich der Circuit Breaker von einer Wiederholungslogik?
Die Wiederholungslogik behandelt einen einzelnen Aufruf, der Circuit Breaker den gesamten Aufrufstrom. Wiederholungen fangen kurze Aussetzer ab, der Breaker stoppt die Pipeline, wenn die Störung anhält.
Entstehen zusätzliche Kosten, während der Circuit offen ist?
Nein. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – abgelehnte Aufrufe erzeugen keine Gebühr. Der Gewinn liegt bei der Auslastung: Threads, die sonst in Timeouts hängen, stehen sofort wieder bereit.
Wie teste ich das Muster ohne echten API-Ausfall?
Mit einer Testfunktion, die kontrolliert Exceptions wirft. Ersetzen Sie solve_captcha im Unit-Test durch einen Platzhalter, der die ersten Aufrufe scheitern lässt, und prüfen Sie die Folge closed → open → half-open → closed. Für die Abklingzeit hilft eine vorgestellte Systemzeit.
Funktioniert das Muster auch für andere CAPTCHA-Typen?
Ja, der Breaker ist typunabhängig. Die Beispiele nutzen reCAPTCHA v2 (userrecaptcha); dieselbe Klasse umschließt ebenso Cloudflare Turnstile, GeeTest v3 oder Bild-CAPTCHAs. Es ändert sich nur der method-Parameter der Übermittlung.
Belastbare CAPTCHA-Pipelines mit CaptchaAI aufbauen
Holen Sie sich Ihren API-Schlüssel auf captchaai.com und legen Sie den Circuit Breaker direkt um Ihre erste Integration.