DevOps & Skalierung

Überwachung der CAPTCHA-Lösungsraten mit Prometheus und Grafana

Eine CAPTCHA-Pipeline im laufenden Betrieb beurteilen Sie an sechs Zeitreihen: Versuche, Erfolge, Fehler nach Code, Lösungsdauer, Guthaben und Warteschlangentiefe. Alles Weitere ist Werkzeug – prometheus_client exportiert die Werte direkt aus Ihrem Solver, Prometheus liest sie alle 10 Sekunden ab, Grafana zeichnet sie, und drei Alarmregeln melden sich, bevor die erste Support-Anfrage eintrifft. Diese Anleitung baut genau diesen Stack: Exporter, Scrape-Konfiguration, Docker Compose, PromQL-Abfragen und Schwellenwerte, die nachts niemanden grundlos wecken.


Sechs Metriken, die eine Solver-Pipeline beschreiben

Fangen Sie nicht beim Dashboard an, sondern bei der Frage, welche Aussage Sie im Störungsfall brauchen. Drei Counter, ein Histogramm und zwei Gauges decken den Alltag ab.

Metrik Typ Aussage
captcha_solves_total Counter alle Lösungsversuche, aufgeschlüsselt nach method
captcha_solves_success Counter erfolgreich zurückgegebene Tokens
captcha_solves_errors Counter Fehlversuche, aufgeschlüsselt nach Fehlercode
captcha_solve_duration Histogramm Verteilung der Lösungszeit
captcha_balance Gauge aktuelles Guthaben in US-Dollar
captcha_queue_length Gauge offene Aufgaben in der Warteschlange

Die Erfolgsquote ist bewusst keine eigene Metrik: Sie entsteht erst in Grafana aus zwei Countern und lässt sich dort für jedes Zeitfenster neu berechnen.


Metriken aus dem Python-Solver exportieren

Der Exporter ist kein eigener Dienst, sondern eine dünne Schicht um den vorhandenen Solver-Aufruf. InstrumentedSolver zählt jeden Versuch, stoppt die Dauer und schreibt das Ergebnis in Counter, Histogramm und Gauge – die Logik gegen in.php und res.php bleibt unverändert. Das Label method trennt die CAPTCHA-Typen, sodass reCAPTCHA v2 und Cloudflare Turnstile im Dashboard später als eigene Serien erscheinen.

Achten Sie auf die Histogramm-Buckets: Sie müssen die reale Spannweite abdecken, sonst landet alles im letzten Bucket und das P95 wird unbrauchbar. Die Staffelung von 5 bis 120 Sekunden passt zu gemischtem Betrieb; reine Bild-CAPTCHAs bräuchten feinere Stufen.

# metrics.py
import time
import requests
from prometheus_client import (
    Counter, Histogram, Gauge, start_http_server,
)


# Define metrics
SOLVES_TOTAL = Counter(
    "captcha_solves_total",
    "Total CAPTCHA solve attempts",
    ["method"],
)

SOLVES_SUCCESS = Counter(
    "captcha_solves_success",
    "Successful CAPTCHA solves",
    ["method"],
)

SOLVES_ERRORS = Counter(
    "captcha_solves_errors",
    "Failed CAPTCHA solves",
    ["method", "error_code"],
)

SOLVE_DURATION = Histogram(
    "captcha_solve_duration_seconds",
    "CAPTCHA solve duration in seconds",
    ["method"],
    buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120],
)

BALANCE = Gauge(
    "captcha_balance_usd",
    "Current CaptchaAI account balance in USD",
)

QUEUE_LENGTH = Gauge(
    "captcha_queue_length",
    "Number of pending CAPTCHA tasks",
)


class InstrumentedSolver:
    """Solver with Prometheus metric instrumentation."""

    def __init__(self, api_key):
        self.api_key = api_key
        self.base = "https://ocr.captchaai.com"

    def solve(self, method, **params):
        """Solve CAPTCHA with metric collection."""
        SOLVES_TOTAL.labels(method=method).inc()
        start = time.time()

        try:
            token = self._do_solve(method, params)
            duration = time.time() - start

            SOLVES_SUCCESS.labels(method=method).inc()
            SOLVE_DURATION.labels(method=method).observe(duration)

            return token

        except Exception as e:
            error_code = str(e)[:30]
            SOLVES_ERRORS.labels(
                method=method, error_code=error_code,
            ).inc()
            raise

    def update_balance(self):
        """Fetch and update balance metric."""
        resp = requests.get(f"{self.base}/res.php", params={
            "key": self.api_key,
            "action": "getbalance",
            "json": 1,
        }, timeout=15)
        balance = float(resp.json()["request"])
        BALANCE.set(balance)
        return balance

    def _do_solve(self, method, params, timeout=120):
        data = {"key": self.api_key, "method": method, "json": 1}
        data.update(params)

        resp = requests.post(
            f"{self.base}/in.php", data=data, timeout=30,
        )
        result = resp.json()

        if result.get("status") != 1:
            raise RuntimeError(result.get("request"))

        task_id = result["request"]
        start = time.time()

        while time.time() - start < timeout:
            time.sleep(5)
            resp = requests.get(f"{self.base}/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1,
            }, timeout=15)
            data = resp.json()
            if data["request"] != "CAPCHA_NOT_READY":
                if data.get("status") == 1:
                    return data["request"]
                raise RuntimeError(data["request"])

        raise TimeoutError("Solve timeout")


# Start metrics server on port 8000
start_http_server(8000)
print("Metrics server running on :8000/metrics")

start_http_server(8000) öffnet einen eigenen Thread, der /metrics ausliefert; der Overhead pro Messpunkt liegt im Mikrosekundenbereich.

Labels sparsam und datenschutzkonform wählen

Der Fehlercode wird im Beispiel auf 30 Zeichen gekürzt – das begrenzt die Zahl der Zeitreihen, falls die API eine lange Fehlermeldung zurückgibt. Nach demselben Prinzip gehören Ziel-URLs, Task-IDs oder IP-Adressen nie in ein Label: Jede Wertkombination erzeugt eine eigene Zeitreihe, die Prometheus über die gesamte Retention behält. Unter der DSGVO ist eine Metrikdatenbank zudem kein Sonderfall – personenbezogene Daten sind dort nur schwerer zu bereinigen.


Prometheus auf den Exporter zeigen lassen

Prometheus holt die Werte aktiv ab; der Solver kennt Prometheus gar nicht. Der Job unten scrapt alle 10 Sekunden und überschreibt damit das globale Intervall von 15 Sekunden – eng genug, um einen Einbruch der Erfolgsquote innerhalb einer Minute zu sehen.

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:

  - job_name: "captcha-solver"
    static_configs:

      - targets: ["solver-app:8000"]
    scrape_interval: 10s

solver-app ist dabei kein DNS-Name, sondern der Servicename aus dem Compose-Netz. Wer localhost einträgt, sucht später lange nach dem Grund, warum das Ziel als „down“ gemeldet wird.


Den kompletten Stack lokal starten

Drei Container genügen als Testumgebung: der instrumentierte Solver, Prometheus als Zeitreihendatenbank und Grafana als Oberfläche. Das Volume grafana-data sorgt dafür, dass Dashboards einen Neustart überleben.

# docker-compose.yml
version: "3.8"

services:
  solver:
    build: .
    environment:

      - CAPTCHAAI_KEY=${CAPTCHAAI_KEY}
    ports:

      - "8000:8000"

  prometheus:
    image: prom/prometheus:latest
    volumes:

      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:

      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    ports:

      - "3000:3000"
    environment:

      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:

      - grafana-data:/var/lib/grafana

volumes:
  grafana-data:

Zwei Punkte gelten nur lokal: Das Grafana-Passwort steht im Klartext in der Compose-Datei, und die Ports 9090 und 3000 sind offen. Auf einem erreichbaren Server gehören Passwort und API-Schlüssel in ein Secret, die Ports hinter einen Reverse-Proxy.


PromQL-Abfragen für das Grafana-Dashboard

Vier Panels reichen zum Start: Erfolgsquote, mittlere Lösungszeit, Fehleraufschlüsselung und Guthaben. Das P95-Panel kommt dazu, sobald mehrere Worker parallel laufen.

Erfolgsquote in Prozent

Zwei Counter ins Verhältnis gesetzt, gemittelt über fünf Minuten:

rate(captcha_solves_success[5m])
/ rate(captcha_solves_total[5m]) * 100

Mittlere Lösungszeit

Summe geteilt durch Anzahl – gut für den Trend, blind für Ausreißer:

rate(captcha_solve_duration_seconds_sum[5m])
/ rate(captcha_solve_duration_seconds_count[5m])

Fehler nach Fehlercode

Als gestapeltes Flächendiagramm zeigt diese Abfrage sofort, ob ein einzelner Fehlercode dominiert oder alle gleichmäßig zunehmen:

sum by (error_code) (
  rate(captcha_solves_errors[5m])
)

Guthabenverlauf

Der Rohwert der Gauge; die Steigung über 24 Stunden verrät den tatsächlichen Verbrauch:

captcha_balance_usd

P95 der Lösungsdauer

Die Kennzahl, an der Sie eine langsam kippende Pipeline zuerst erkennen:

histogram_quantile(0.95,
  rate(captcha_solve_duration_seconds_bucket[5m])
)

Alarmregeln, die sich wirklich lohnen

Drei Regeln decken die häufigsten Ausfallarten ab: leeres Guthaben, gehäufte Fehler und schleichende Verlangsamung. Die for-Angabe ist dabei wichtiger als der Schwellenwert – sie verhindert, dass ein einzelner Ausschlag jemanden aus dem Feierabend holt.

# alert_rules.yml
groups:

  - name: captcha-alerts
    rules:

      - alert: LowBalance
        expr: captcha_balance_usd < 5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CaptchaAI balance below $5"

      - alert: HighErrorRate
        expr: |
          rate(captcha_solves_errors[5m])
          / rate(captcha_solves_total[5m]) > 0.1
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "CAPTCHA error rate above 10%"

      - alert: SlowSolveTime
        expr: |
          histogram_quantile(0.95,
            rate(captcha_solve_duration_seconds_bucket[5m])
          ) > 60
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "P95 solve time exceeds 60s"

Die Schwellen sind Startwerte, keine Naturkonstanten. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro gelöstem CAPTCHA, und jeder Tarif enthält unbegrenzte Lösungen pro Thread. Wer BASIC (15 $/Monat, 5 Threads) fährt, kommt mit einer niedrigen Guthabenschwelle aus; bei ADVANCE (90 $/Monat, 50 Threads) sollte der Alarm früher greifen, weil ein leeres Konto fünfzig parallele Aufgaben auf einmal stoppt. Auch SlowSolveTime braucht Kontext: Wer überwiegend reCAPTCHA v2 löst, sieht regelmäßig Dauern, die bei reinem Turnstile-Betrieb längst ein Vorfall wären.


Aus dem Betrieb: der stille Retry-Sturm

Ein Wiener Team betreibt seine Solver-Worker in Kubernetes und rollt über GitLab CI aus; gebucht ist PREMIUM (170 $/Monat, 100 Threads). Nach einem Frontend-Update auf der Zielanwendung stieg nicht die Fehlerquote, sondern nur das P95 der Lösungsdauer – von rund 20 auf über 50 Sekunden. Der HighErrorRate-Alarm blieb still, weil am Ende fast jeder Versuch ein Token lieferte.

Spürbar war trotzdem etwas: Der Durchsatz halbierte sich. Jede Anfrage belegte länger einen Thread, captcha_queue_length wuchs, die nachgelagerte Verarbeitung lief in ihr Timeout. Sichtbar wurde die Ursache erst, als P95 und Warteschlangentiefe im selben Panel übereinanderlagen. Eine Pipeline fällt selten aus – sie wird langsam.


Wenn keine Daten ankommen

Symptom Ursache Abhilfe
/metrics liefert nichts Exporter-Server nicht gestartet start_http_server(8000) vor der Solver-Schleife aufrufen
Prometheus meldet das Ziel als „down“ falsche Zieladresse Servicename statt localhost eintragen
Grafana zeigt leere Panels Datenquelle fehlt Prometheus unter http://prometheus:9090 anlegen
Werte fallen nach jedem Deploy auf null Counter-Reset ist normal rate() abfragen, nie den Rohzähler
Erfolgsquote schwankt stark zu kurzes Zeitfenster Fenster auf [15m] oder [1h] vergrößern

Häufige Fragen

Warum unterscheidet sich meine Erfolgsquote je nach Zeitfenster?

Weil rate() über das gewählte Fenster mittelt. Bei zehn Lösungen pro Minute verschiebt ein einzelner Fehlversuch die Quote in [5m] um mehrere Prozentpunkte, in [1h] kaum noch. Kurze Fenster gehören in Alarme, lange in Berichte.

Lässt sich aus den Metriken ablesen, wie viele Threads ich brauche?

Näherungsweise ja: Lösungen pro Sekunde mal mittlere Lösungsdauer ergibt die durchschnittlich belegten Threads. Liegt der Wert dauerhaft nahe am Kontingent und wächst captcha_queue_length in Spitzen, ist der nächstgrößere Tarif fällig – etwa ADVANCE (90 $/Monat, 50 Threads) auf PREMIUM (170 $/Monat, 100 Threads).

Kann ich einzelne CAPTCHA-Typen getrennt auswerten?

Ja, über das Label method: Mit by (method) erscheinen Turnstile, reCAPTCHA v2 und Bild-CAPTCHAs als getrennte Serien. Das ist auch inhaltlich nötig – Turnstile wird typischerweise in unter 10 Sekunden gelöst, reCAPTCHA v2 braucht bis zu einer Minute.

Welche Labels sollte ich unbedingt vermeiden?

Alles mit hoher Kardinalität: vollständige URLs, Task-IDs, Zeitstempel, Nutzerkennungen. Zwei bis drei Labels mit überschaubarem Wertebereich – method, error_code, optional die Worker-Instanz – reichen aus.

Brauche ich dafür eigene Server?

Nein. Für einen Worker-Pool genügt ein kleiner VPS bei Hetzner, IONOS oder netcup; mit 15 Tagen Retention bleibt der Speicherbedarf im Gigabyte-Bereich. Erst wenn mehrere Teams dieselbe Instanz nutzen, lohnt ein separater Metrik-Host.


Weiterlesen


Messwerte statt Vermutungen – Ihre CAPTCHA-Pipeline mit CaptchaAI beobachten.

Kommentare sind für diesen Artikel deaktiviert.