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.
Trends mit PromQL auswerten
# 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.