Tutorials

Fehlerbudgetverfolgung für CAPTCHA-Lösungszuverlässigkeit

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

Kommentare sind für diesen Artikel deaktiviert.