Tutorials

Zeitreihendaten für CAPTCHA-Lösungsleistungstrends

Erfolgsquote, Lösungszeit als Perzentil, Fehler pro Fehlercode und API-Guthaben – mit diesen vier Serien behalten Sie den CAPTCHA-Betrieb im Griff. Wer sie im Minutentakt in eine Zeitreihen-Datenbank schreibt, sieht eine Verschlechterung entstehen, statt sie am nächsten Morgen aus einer vollgelaufenen Warteschlange zu rekonstruieren.

Der Unterschied liegt nicht im Messen, sondern in der Zeitachse: Ein Dashboard-Snapshot zeigt den Moment, eine Zeitreihe zeigt, seit wann es schlechter wird und ob der Knick mit einem Deployment zusammenfällt. Ausfälle sind nämlich selten ein Schalter, der umgelegt wird – erst klettert das P95 der Lösungszeit über drei Tage von 18 auf 34 Sekunden, dann häufen sich Timeouts in der Anwendung.

Welche Zeitreihen-Datenbank zu Ihrem Stack passt

Diese Entscheidung fällt vor der Instrumentierung, denn sie prägt das Metrikmodell: Prometheus fragt Endpunkte ab und aggregiert über Labels, InfluxDB und TimescaleDB nehmen einzelne Messpunkte entgegen.

Kriterium Prometheus InfluxDB TimescaleDB
Stärke Betriebsüberwachung und Alarmierung Metriken mit hoher Kardinalität Auswertung mit SQL
Abfragesprache PromQL Flux SQL
Aufbewahrung über die Konfiguration über Retention-Policies wie in PostgreSQL
Grafana-Anbindung nativ nativ nativ
Einstiegshürde niedrig mittel niedrig, wenn SQL sitzt

Für Teams im DACH-Raum zählt meist die Betriebsfrage mehr als die Feature-Liste: Alle drei laufen auf einer kleinen Instanz bei Hetzner oder netcup und lassen sich aus GitLab CI heraus versionieren. Wer Prometheus ohnehin betreibt, hängt die CAPTCHA-Metriken dort an.

Diese CAPTCHA-Kennzahlen gehören in die Zeitreihe

Kennzahl Metriktyp Wozu
Erfolgsquote (%) Gauge Qualitätsänderungen früh erkennen
Lösungszeit (ms) Histogramm Verlangsamungen sehen, Timeouts sinnvoll setzen
Fehler pro Fehlercode Counter neue Fehlermuster identifizieren
Kosten pro Lösung ($) Gauge Budgetkontrolle, Ausreißer erkennen
Warteschlangentiefe Gauge Kapazitätsplanung
Vor Verwendung abgelaufene Tokens Counter Signal für die TTL-Feinjustierung
API-Guthaben Gauge Auslöser zum Nachladen

Die Kostenzeile wird oft falsch modelliert. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – jeder Plan enthält unbegrenzte Lösungen pro Thread. Eine Konstante „Preis pro Lösung“ ist deshalb nur eine Hilfsgröße, die den Planpreis auf das Monatsvolumen umlegt: Bei BASIC (15 $/Monat, 5 Threads) und rund 100.000 Lösungen sind das etwa 0,15 $ pro 1.000 Lösungen, bei ADVANCE (90 $/Monat, 50 Threads) und einer Million Lösungen etwa 0,09 $ pro 1.000. Preise in US-Dollar.

Solver mit Prometheus instrumentieren

Der Weg über ein Push Gateway passt zu kurzlebigen Prozessen: Einen Scraping-Job mit 20 Minuten Laufzeit fragt Prometheus nie zuverlässig ab. Der Solver misst deshalb selbst und schiebt die Werte weiter.

Metriken direkt im Solver erfassen

import os
import time
import requests
from prometheus_client import CollectorRegistry, Counter, Histogram, Gauge, push_to_gateway

registry = CollectorRegistry()

SOLVE_TOTAL = Counter(
    "captcha_solve_total", "Total CAPTCHA solve attempts",
    ["type", "status"], registry=registry
)
SOLVE_LATENCY = Histogram(
    "captcha_solve_latency_seconds", "CAPTCHA solve latency",
    ["type"], buckets=[5, 10, 15, 20, 30, 45, 60, 90, 120],
    registry=registry
)
SOLVE_COST = Counter(
    "captcha_solve_cost_dollars", "Total cost of CAPTCHA solves",
    ["type"], registry=registry
)
API_BALANCE = Gauge(
    "captcha_api_balance_dollars", "CaptchaAI account balance",
    registry=registry
)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]
PUSHGATEWAY = os.environ.get("PUSHGATEWAY_URL", "localhost:9091")


def solve_with_metrics(sitekey, pageurl, captcha_type="recaptcha_v2"):
    start = time.time()

    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:
        SOLVE_TOTAL.labels(type=captcha_type, status="submit_error").inc()
        push_metrics()
        return {"error": data.get("request")}

    captcha_id = data["request"]

    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:
            elapsed = time.time() - start
            SOLVE_TOTAL.labels(type=captcha_type, status="solved").inc()
            SOLVE_LATENCY.labels(type=captcha_type).observe(elapsed)
            SOLVE_COST.labels(type=captcha_type).inc(0.00299)
            push_metrics()
            return {"solution": result["request"]}

        if result.get("request") != "CAPCHA_NOT_READY":
            SOLVE_TOTAL.labels(type=captcha_type, status="error").inc()
            push_metrics()
            return {"error": result.get("request")}

    SOLVE_TOTAL.labels(type=captcha_type, status="timeout").inc()
    push_metrics()
    return {"error": "TIMEOUT"}


def push_metrics():
    try:
        push_to_gateway(PUSHGATEWAY, job="captcha_solver", registry=registry)
    except Exception:
        pass  # Don't fail solving because metrics push failed


def update_balance():
    resp = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "getbalance"
    })
    try:
        balance = float(resp.text)
        API_BALANCE.set(balance)
        push_metrics()
    except ValueError:
        pass

Zwei Details entscheiden über die Datenqualität: Die Stoppuhr startet vor der Übermittlung, sonst fehlt die Wartezeit in jeder Messung, und jeder Ausgang bekommt ein eigenes status-Label (solved, error, submit_error, timeout). push_metrics() fängt jede Ausnahme ab: Ein fehlgeschlagener Push darf den Lösungsvorgang nie abbrechen.

# Success rate over last hour
rate(captcha_solve_total{status="solved"}[1h])
/ rate(captcha_solve_total[1h]) * 100

# P95 solve latency
histogram_quantile(0.95, rate(captcha_solve_latency_seconds_bucket[1h]))

# Error rate by type
rate(captcha_solve_total{status="error"}[1h])

# Hourly cost
increase(captcha_solve_cost_dollars_total[1h])

Die Erfolgsquote entsteht als Verhältnis zweier Raten über dasselbe Fenster – nur so bleibt der Wert nach einem Prozess-Neustart brauchbar. Bei der Lösungszeit sagt das P95 mehr als der Mittelwert: Ein Median von 12 s bei einem P95 von 70 s beschreibt einen anderen Betrieb als 12 s bei 20 s.

InfluxDB als eigenständiger Metrik-Speicher

Sollen die Metriken nicht in ein bestehendes Prometheus fließen, ist InfluxDB der direktere Weg: Jeder Solve wird zu einem Messpunkt aus Tags und Feldern.

Messpunkte pro Lösung schreiben

from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS

INFLUX_URL = os.environ.get("INFLUX_URL", "http://localhost:8086")
INFLUX_TOKEN = os.environ.get("INFLUX_TOKEN", "")
INFLUX_ORG = os.environ.get("INFLUX_ORG", "captcha")
INFLUX_BUCKET = os.environ.get("INFLUX_BUCKET", "captcha_metrics")

influx_client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = influx_client.write_api(write_options=SYNCHRONOUS)


def record_solve_metric(captcha_type, status, elapsed_ms, cost=0.0, error=None):
    point = (
        Point("captcha_solve")
        .tag("type", captcha_type)
        .tag("status", status)
        .field("elapsed_ms", elapsed_ms)
        .field("cost", cost)
        .field("success", 1 if status == "solved" else 0)
    )
    if error:
        point = point.tag("error_code", error)
    write_api.write(bucket=INFLUX_BUCKET, record=point)


def record_balance(balance):
    point = Point("captcha_balance").field("balance", balance)
    write_api.write(bucket=INFLUX_BUCKET, record=point)

Tags sind indiziert, Felder nicht: type, status und error_code gehören als Tag in den Punkt, Dauer und Kosten als Feld.

Trendabfragen in Flux

// Success rate over last 24 hours (1-hour windows)
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "success")
  |> aggregateWindow(every: 1h, fn: mean)
  |> map(fn: (r) => ({r with _value: r._value * 100.0}))
  |> yield(name: "success_rate")

// Average solve time by type
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "elapsed_ms" and r.status == "solved")
  |> group(columns: ["type"])
  |> aggregateWindow(every: 1h, fn: mean)
  |> yield(name: "avg_latency")

// Cumulative cost
from(bucket: "captcha_metrics")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "captcha_solve" and r._field == "cost")
  |> cumulativeSum()
  |> yield(name: "cumulative_cost")

aggregateWindow legt die Auflösung fest: Stundenfenster für den Wochenblick, Fünfminutenfenster für die Fehlersuche am laufenden Tag. Häufen sich erneute Versuche, sieht man das zuerst an der kumulierten Kostenkurve: Sie knickt nach oben, lange bevor ein Alarm anspringt.

Node.js-Worker mit eigenem Metrik-Endpunkt

Langlaufende Node.js-Worker brauchen kein Push Gateway. Sie halten ein Register im Prozess und stellen /metrics bereit, Prometheus fragt den Endpunkt in seinem eigenen Takt ab.

const client = require("prom-client");
const axios = require("axios");

const register = new client.Registry();
const API_KEY = process.env.CAPTCHAAI_API_KEY;

const solveTotal = new client.Counter({
  name: "captcha_solve_total",
  help: "Total CAPTCHA solve attempts",
  labelNames: ["type", "status"],
  registers: [register],
});

const solveLatency = new client.Histogram({
  name: "captcha_solve_latency_seconds",
  help: "CAPTCHA solve latency",
  labelNames: ["type"],
  buckets: [5, 10, 15, 20, 30, 45, 60, 90, 120],
  registers: [register],
});

async function solveWithMetrics(sitekey, pageurl, type = "recaptcha_v2") {
  const start = Date.now();

  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) {
    solveTotal.inc({ type, status: "submit_error" });
    return { error: submit.data.request };
  }

  const captchaId = submit.data.request;

  for (let i = 0; i < 60; 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: captchaId, json: 1 },
    });

    if (poll.data.status === 1) {
      const elapsed = (Date.now() - start) / 1000;
      solveTotal.inc({ type, status: "solved" });
      solveLatency.observe({ type }, elapsed);
      return { solution: poll.data.request };
    }

    if (poll.data.request !== "CAPCHA_NOT_READY") {
      solveTotal.inc({ type, status: "error" });
      return { error: poll.data.request };
    }
  }

  solveTotal.inc({ type, status: "timeout" });
  return { error: "TIMEOUT" };
}

// Expose metrics endpoint
const express = require("express");
const app = express();
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", register.contentType);
  res.end(await register.metrics());
});
app.listen(9090);

Läuft der Worker mehrfach, wird jede Instanz einzeln abgefragt. Ein Label pro Instanz genügt; jedes weitere kostet nur Kardinalität.

Baseline, Schwellenwerte und Aufbewahrung

Ohne Baseline ist jeder Alarm geraten. Sammeln Sie zwei Wochen Normalbetrieb und leiten Sie die Schwellen aus den eigenen Werten ab:

  • Erfolgsquote: Alarm, wenn der gleitende Ein-Stunden-Wert deutlich unter die Baseline fällt – zehn Prozentpunkte sind ein brauchbarer Startwert.
  • Lösungszeit: Alarm auf das P95, nicht auf Einzelwerte.
  • Fehlercodes: auch auf neue Codes alarmieren, nicht nur auf Volumen.
  • API-Guthaben: Vorlauf von einem Arbeitstag zum Nachladen einplanen.

Bei der Aufbewahrung bewährt sich eine dreistufige Staffel: Rohwerte 7 Tage, Stundenaggregate 90 Tage, Tagesaggregate dauerhaft. Dazu ein DSGVO-Hinweis: Ziel-URLs, Kundenkennungen und IP-Adressen haben in Labels nichts zu suchen. IP-Adressen gelten als personenbezogene Daten, und Metrikserien werden selten so sorgfältig gelöscht wie Anwendungsdatenbanken.

Praxisbeispiel: drei Nächte schleichender Anstieg

Ein Münchner Team fährt seine QA-Läufe gegen den eigenen Shopware-Staging-Shop: Worker auf zwei kleinen Hetzner-Instanzen, angestoßen von einem nächtlichen GitLab-CI-Job, beim Login reCAPTCHA v2, Tokens per API.

In der Zeitreihe stieg das P95 der Lösungszeit über drei Nächte von 21 auf 39 Sekunden, während Median und Erfolgsquote stabil blieben – ein reines Latenzproblem. Die Ursache lag im eigenen Ablauf: Ein neuer CI-Schritt startete alle Testfälle gleichzeitig, sodass mehr Anfragen parallel liefen, als das Thread-Kontingent abdeckte; die Wartezeit landete in der Gesamtdauer. Gestaffelter Start und ein am realen Bedarf ausgerichtetes Kontingent genügten.

Typische Fehlerbilder in der Messkette

Symptom Ursache Vorgehen
Lücken in den Diagrammen Das Push Gateway erhält keine Daten Netzwerkpfad zwischen Worker und Gateway prüfen, Push-Fehler protokollieren
Perzentile wirken unplausibel Histogramm-Buckets passen nicht zum Workload Bucket-Grenzen an die real gemessenen Lösungszeiten anpassen
Abfragen werden langsam zu viele Label-Werte, die Kardinalität explodiert Labels auf type, status und error_code begrenzen
Token wird geliefert, aber vom Ziel abgelehnt sitekey, pageurl oder Sitzungskontext passen nicht Parameter neu erfassen, Token in derselben Sitzung einsetzen

FAQ

Welche Metriken reichen für den Anfang?

Drei genügen: Erfolgsquote, Lösungszeit als Histogramm und Fehler nach Code. Damit beantworten Sie „läuft es“, „läuft es langsamer“ und „woran liegt es“. Kosten, Warteschlangentiefe und Guthaben kommen dazu, sobald mehrere Worker parallel arbeiten.

Wie lange sollten CAPTCHA-Metriken aufbewahrt werden?

Rohwerte 7 Tage, Stundenaggregate 90 Tage, Tagesaggregate dauerhaft. Hochauflösende Daten brauchen Sie nur für die akute Fehlersuche, für Kapazitätsplanung und Jahresvergleich genügen Aggregate.

Wie bilde ich die Kosten ab, wenn pro Thread abgerechnet wird?

Legen Sie den Monatspreis Ihres Plans auf die Lösungen des Monats um und schreiben Sie diesen Wert in die Serie. Da pro Thread unbegrenzte Lösungen enthalten sind, sinkt der rechnerische Preis mit steigendem Volumen: Die Kurve zeigt Auslastung, nicht die Rechnung.

Ab wann wird die Kardinalität zum Problem?

Sobald ein Label freie Werte annimmt: Ziel-URL, Task-ID oder Zeitstempel erzeugen pro Lösung eine neue Serie. Bleiben Sie bei wenigen Labels mit bekanntem Wertebereich – Detailreiches gehört ins Log.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.