Ein Fehlerbudget beantwortet die Frage, die eine reine Erfolgsquote offenlässt: Wie viele fehlgeschlagene Lösungen darf Ihre CAPTCHA-Pipeline sich leisten, bevor sie das Zuverlässigkeitsziel verfehlt? Die Antwort ist eine konkrete Zahl. Sie legen ein SLO fest – etwa 95 % erfolgreiche Lösungen – und der Rest, die zulässigen 5 %, ist Ihr Budget für Ausfälle. Solange das Budget nicht aufgebraucht ist, läuft die Pipeline im grünen Bereich; ist es erschöpft, stoppen Sie riskante Deployments und gehen den Fehlerursachen nach. Dieser Leitfaden zeigt, wie Sie ein Fehlerbudget für die CAPTCHA-Lösung definieren, den Verbrauch in einem rollenden Fenster messen und in Python und JavaScript automatisch darauf reagieren.
Was ist ein Fehlerbudget?
| Begriff | Definition | Beispiel |
|---|---|---|
| SLO | Angestrebte Erfolgsquote | 95 % erfolgreiche Lösungen |
| Fehlerbudget | Zulässiger Anteil an Ausfällen | 5 % aller Lösungen dürfen fehlschlagen |
| Brennrate | Tempo, mit dem das Budget verbraucht wird | 2× bedeutet: Budget nach der halben Fensterdauer leer |
| Fenster | Messzeitraum | rollend 24 Stunden oder 7 Tage |
Der Reiz des Modells liegt darin, dass es Zuverlässigkeit von einem Gefühl in eine Buchhaltung verwandelt. Statt zu diskutieren, ob 94,2 % „schlimm genug" sind, prüfen Sie einfach, wie viel vom Budget noch übrig ist – und ob die Brennrate ein Eingreifen rechtfertigt.
Fehlerbudget berechnen: vom SLO zur konkreten Zahl
Rechnen Sie das SLO in absolute Zahlen um, dann wird das Budget greifbar. Bei einem 24-Stunden-Fenster mit 10.000 Lösungen und einem SLO von 95 % beträgt Ihr Fehlerbudget 500 fehlgeschlagene Lösungen. Sobald diese 500 erreicht sind, sollten neue Bereitstellungen und riskante Änderungen pausieren, bis sich das Fenster wieder entlastet.
Ein Beispiel aus dem DACH-Alltag: Ein Preismonitoring-Team bei einem deutschen Online-Händler (Shopware-Stack, Worker auf Hetzner-VPS) verarbeitet 50.000 CAPTCHA-Lösungen pro Tag. Bei einem SLO von 95 % ergibt das ein Budget von 2.500 zulässigen Fehlern. Das Team verdrahtet den Budget-Status mit seiner GitLab-CI-Pipeline: Meldet der Tracker den Status exhausted, friert ein Deployment-Gate automatisch neue Releases ein. So wird aus einer abstrakten SLO-Zahl eine Betriebsregel, die niemand manuell durchsetzen muss. Wie schnell Sie das Budget nachfüllen können, hängt auch von der Thread-Zahl Ihres CaptchaAI-Plans ab – mehr parallele Threads bedeuten mehr Durchsatz pro Minute, ohne die Fehlerquote zu verändern.
Brennrate als Frühwarnsignal
Die Brennrate ist der Frühindikator: Sie zeigt, ob das Budget schneller schwindet als geplant, lange bevor es tatsächlich leer ist. Ein Wert von 1,0 heißt, dass Sie das Budget genau zum Fensterende ausschöpfen – alles darüber ist ein Warnsignal. Deshalb lohnt es sich, die Brennrate zu verstehen, bevor Sie den Tracker in Betrieb nehmen.
| Brennrate | Bedeutung | Empfohlene Reaktion |
|---|---|---|
| < 1,0 | Budget wird langsamer verbraucht als geplant | Kein Eingriff nötig |
| 1,0 | Budget reicht exakt bis zum Fensterende | Aufmerksam beobachten |
| 2,0 | Budget nach der halben Fensterdauer erschöpft | Ursache prüfen und Tempo drosseln |
| 5,0+ | Sehr schneller Budgetverbrauch | Unkritische Lösungen pausieren |
Fehlerbudgets pro CAPTCHA-Typ trennen
Ein gemeinsames Budget über alle CAPTCHA-Typen verschleiert typspezifische Probleme. reCAPTCHA v2, Cloudflare Turnstile, GeeTest v3 und Bild-CAPTCHAs erreichen in der Praxis unterschiedliche Erfolgsquoten, weil sie unterschiedlich anspruchsvoll sind. Führen Sie deshalb je Typ einen eigenen Tracker mit eigenem SLO – etwa ein strengeres Ziel für Turnstile und ein moderateres für schwierigere Bild-Raster. So sehen Sie sofort, welcher Typ ein Budget aufzehrt, statt einen aggregierten Wert zu interpretieren, der die Ursache verdeckt. Messen Sie zuerst Ihre reale Baseline pro Typ und leiten Sie das SLO daraus ab, statt einen Zielwert zu raten.
Fehlerbudget-Tracker in Python
Die folgende Klasse führt ein rollendes Fenster über alle Lösungsversuche, berechnet das verbleibende Budget und die Brennrate und ruft bei jedem Statuswechsel registrierte Callbacks auf – ideal, um Alerts oder eine Drosselung auszulösen.
import time
import threading
from dataclasses import dataclass, field
from collections import deque
from enum import Enum
API_KEY = "YOUR_API_KEY"
class BudgetStatus(Enum):
HEALTHY = "healthy" # Budget > 50% remaining
WARNING = "warning" # Budget 10-50% remaining
CRITICAL = "critical" # Budget < 10% remaining
EXHAUSTED = "exhausted" # Budget depleted
@dataclass
class SLOConfig:
"""Service Level Objective configuration."""
target_success_rate: float = 0.95 # 95%
window_seconds: int = 86400 # 24 hours
warning_threshold: float = 0.50 # Alert at 50% budget
critical_threshold: float = 0.10 # Alert at 10% budget
@dataclass
class ErrorBudgetEvent:
timestamp: float
success: bool
class ErrorBudgetTracker:
"""Tracks error budget consumption for CAPTCHA solving."""
def __init__(self, config: SLOConfig = SLOConfig()):
self.config = config
self._events: deque[ErrorBudgetEvent] = deque()
self._lock = threading.Lock()
self._callbacks: dict[BudgetStatus, list[callable]] = {
status: [] for status in BudgetStatus
}
self._last_status = BudgetStatus.HEALTHY
def on_status_change(self, status: BudgetStatus, callback: callable):
"""Register a callback for status transitions."""
self._callbacks[status].append(callback)
def record(self, success: bool):
"""Record a solve attempt."""
now = time.monotonic()
event = ErrorBudgetEvent(timestamp=now, success=success)
with self._lock:
self._events.append(event)
self._prune(now)
new_status = self._compute_status()
if new_status != self._last_status:
self._last_status = new_status
for cb in self._callbacks.get(new_status, []):
try:
cb(self.get_report())
except Exception as e:
print(f"[BUDGET] Callback error: {e}")
def _prune(self, now: float):
"""Remove events outside the window."""
cutoff = now - self.config.window_seconds
while self._events and self._events[0].timestamp < cutoff:
self._events.popleft()
def _compute_status(self) -> BudgetStatus:
remaining = self.remaining_fraction
if remaining <= 0:
return BudgetStatus.EXHAUSTED
if remaining < self.config.critical_threshold:
return BudgetStatus.CRITICAL
if remaining < self.config.warning_threshold:
return BudgetStatus.WARNING
return BudgetStatus.HEALTHY
@property
def total_events(self) -> int:
with self._lock:
return len(self._events)
@property
def success_count(self) -> int:
with self._lock:
return sum(1 for e in self._events if e.success)
@property
def failure_count(self) -> int:
with self._lock:
return sum(1 for e in self._events if not e.success)
@property
def current_success_rate(self) -> float:
total = self.total_events
return self.success_count / total if total > 0 else 1.0
@property
def error_budget_total(self) -> float:
"""Total allowed failures in the window."""
total = self.total_events
if total == 0:
return 0
return total * (1 - self.config.target_success_rate)
@property
def error_budget_remaining(self) -> float:
"""Remaining failure allowance."""
return max(0, self.error_budget_total - self.failure_count)
@property
def remaining_fraction(self) -> float:
"""Fraction of error budget remaining (0.0 to 1.0)."""
budget = self.error_budget_total
if budget <= 0:
return 1.0 if self.failure_count == 0 else 0.0
return max(0, self.error_budget_remaining / budget)
@property
def burn_rate(self) -> float:
"""How fast the budget is being consumed (1.0 = normal, 2.0 = 2× faster)."""
total = self.total_events
if total == 0:
return 0.0
expected_failures = total * (1 - self.config.target_success_rate)
if expected_failures == 0:
return 0.0
return self.failure_count / expected_failures
def get_report(self) -> dict:
return {
"status": self._last_status.value,
"slo_target": self.config.target_success_rate,
"current_rate": round(self.current_success_rate, 4),
"total_events": self.total_events,
"successes": self.success_count,
"failures": self.failure_count,
"budget_total": round(self.error_budget_total, 1),
"budget_remaining": round(self.error_budget_remaining, 1),
"budget_remaining_pct": round(self.remaining_fraction * 100, 1),
"burn_rate": round(self.burn_rate, 2),
}
# --- Integration with solver ---
budget = ErrorBudgetTracker(SLOConfig(
target_success_rate=0.95,
window_seconds=3600, # 1-hour window for demo
))
# Register alerts
budget.on_status_change(BudgetStatus.WARNING, lambda r:
print(f"[ALERT] Budget warning: {r['budget_remaining_pct']}% remaining"))
budget.on_status_change(BudgetStatus.CRITICAL, lambda r:
print(f"[ALERT] Budget critical: {r['budget_remaining_pct']}% remaining"))
budget.on_status_change(BudgetStatus.EXHAUSTED, lambda r:
print(f"[ALERT] Budget EXHAUSTED — throttle new requests"))
def solve_with_budget(params: dict) -> str:
"""Solve CAPTCHA while tracking error budget."""
import requests
if budget._last_status == BudgetStatus.EXHAUSTED:
raise RuntimeError("Error budget exhausted — solving paused")
try:
submit_params = {**params, "key": API_KEY, "json": 1}
resp = requests.post(
"https://ocr.captchaai.com/in.php", data=submit_params, timeout=30
).json()
if resp.get("status") != 1:
budget.record(False)
raise RuntimeError(f"Submit: {resp.get('request')}")
task_id = resp["request"]
start = time.monotonic()
while time.monotonic() - start < 180:
time.sleep(5)
poll = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
budget.record(True)
return poll["request"]
budget.record(False)
raise RuntimeError(f"Solve: {poll.get('request')}")
budget.record(False)
raise RuntimeError("Timeout")
except Exception:
budget.record(False)
raise
# Usage
for i in range(100):
try:
token = solve_with_budget({
"method": "turnstile",
"sitekey": "0x4XXXXXXXXXXXXXXXXX",
"pageurl": "https://example.com",
})
except RuntimeError as e:
if "exhausted" in str(e):
print(f"Stopped at iteration {i}")
break
print(budget.get_report())
Fehlerbudget-Tracker in JavaScript
Dieselbe Logik für Node.js-Worker: private Felder halten Fenster und Konfiguration gekapselt, record() prunet alte Ereignisse und feuert Callbacks nur beim Statuswechsel.
class ErrorBudgetTracker {
#events = [];
#config;
#callbacks = {};
constructor(config = {}) {
this.#config = {
targetRate: config.targetRate || 0.95,
windowMs: config.windowMs || 3600_000,
warningThreshold: config.warningThreshold || 0.5,
criticalThreshold: config.criticalThreshold || 0.1,
};
this.lastStatus = "healthy";
}
on(status, callback) {
this.#callbacks[status] = this.#callbacks[status] || [];
this.#callbacks[status].push(callback);
}
record(success) {
const now = Date.now();
this.#events.push({ time: now, success });
this.#prune(now);
const newStatus = this.#computeStatus();
if (newStatus !== this.lastStatus) {
this.lastStatus = newStatus;
for (const cb of this.#callbacks[newStatus] || []) {
cb(this.report());
}
}
}
#prune(now) {
const cutoff = now - this.#config.windowMs;
while (this.#events.length && this.#events[0].time < cutoff) {
this.#events.shift();
}
}
#computeStatus() {
const frac = this.remainingFraction;
if (frac <= 0) return "exhausted";
if (frac < this.#config.criticalThreshold) return "critical";
if (frac < this.#config.warningThreshold) return "warning";
return "healthy";
}
get total() { return this.#events.length; }
get successes() { return this.#events.filter((e) => e.success).length; }
get failures() { return this.#events.filter((e) => !e.success).length; }
get currentRate() { return this.total ? this.successes / this.total : 1; }
get budgetTotal() {
return this.total * (1 - this.#config.targetRate);
}
get budgetRemaining() {
return Math.max(0, this.budgetTotal - this.failures);
}
get remainingFraction() {
const bt = this.budgetTotal;
if (bt <= 0) return this.failures === 0 ? 1 : 0;
return Math.max(0, this.budgetRemaining / bt);
}
get burnRate() {
const expected = this.total * (1 - this.#config.targetRate);
return expected > 0 ? this.failures / expected : 0;
}
report() {
return {
status: this.lastStatus,
currentRate: Math.round(this.currentRate * 10000) / 10000,
total: this.total,
failures: this.failures,
budgetRemainingPct: Math.round(this.remainingFraction * 1000) / 10,
burnRate: Math.round(this.burnRate * 100) / 100,
};
}
}
// Usage
const budget = new ErrorBudgetTracker({ targetRate: 0.95, windowMs: 3600_000 });
budget.on("warning", (r) => console.log(`[WARN] ${r.budgetRemainingPct}% budget left`));
budget.on("exhausted", (r) => console.log("[ALERT] Budget exhausted!"));
// Record results from your solver
budget.record(true); // success
budget.record(false); // failure
console.log(budget.report());
Typische Probleme beim Error-Budget-Tracking
| Problem | Ursache | Lösung |
|---|---|---|
| Budget zu schnell aufgebraucht | SLO zu streng für die realen Bedingungen | SLO an historischen Messwerten ausrichten |
| Budget wird nie verbraucht | SLO zu großzügig gesetzt | SLO verschärfen, um Zuverlässigkeitsverbesserungen zu erzwingen |
| Status springt zwischen den Stufen | Messfenster zu kurz | Längeres Fenster wählen (24 h statt 1 h) |
| Brennrate bei geringem Volumen irreführend | Wenige Ereignisse verzerren die Rechnung | Mindestanzahl an Ereignissen vor der Berechnung verlangen |
| Speicherbedarf des Trackers wächst | Alte Ereignisse werden nicht entfernt | Prüfen, dass _prune bei jedem record() läuft |
Häufige Fragen
Was ist der Unterschied zwischen SLO, SLA und Fehlerbudget?
Das SLO ist Ihr internes Zielniveau (etwa 95 % erfolgreiche Lösungen), das SLA die vertraglich zugesicherte Untergrenze gegenüber Kunden, und das Fehlerbudget der Spielraum dazwischen: die Menge an Ausfällen, die Sie sich leisten können, ohne das SLO zu verfehlen. Im Betrieb steuern Sie über das SLO und das daraus abgeleitete Budget.
Wie lang sollte das Messfenster für ein Fehlerbudget sein?
Ein rollendes 24-Stunden-Fenster ist für die meisten CAPTCHA-Pipelines ein guter Startpunkt. Kürzere Fenster (1 Stunde) reagieren schneller, springen aber bei wenig Traffic zwischen den Statusstufen; längere Fenster (7 Tage) glätten Ausreißer, verzögern jedoch Alerts. Wählen Sie das Fenster passend zu Ihrem Tagesvolumen.
Wie richte ich Alerts auf den Fehlerbudget-Verbrauch ein?
Exportieren Sie Erfolgs- und Fehlerzahlen als Metriken und lassen Sie Prometheus die Brennrate berechnen. Ein Grafana-Alert, der bei einem Budgetverbrauch über 80 % oder einer Brennrate über 2,0 auslöst, meldet Probleme, bevor das Budget vollständig erschöpft ist. Verknüpfen Sie den Alert mit Ihrer On-Call-Rotation.
Was tun, wenn das Fehlerbudget aufgebraucht ist?
Reagieren Sie abgestuft – vom Alert an das Team über das Drosseln neuer Anfragen bis zum Pausieren unkritischer Lösungen. Frieren Sie riskante Deployments ein, bis die Fehlerursache behoben ist. Ein erschöpftes Budget sollte nie stillschweigend ignoriert werden.
Verwandte Leitfäden
- CAPTCHA-Lösungsraten mit Prometheus und Grafana überwachen
- Zeitreihen-Trends der CAPTCHA-Lösung analysieren
- Strukturiertes Logging für CAPTCHA-Vorgänge