Ein Health-Check-Endpunkt beantwortet dem Orchestrator genau eine Frage: Darf dieser Worker weiter CAPTCHA-Aufgaben bekommen? Für einen CAPTCHA-Löser reicht dafür ein simpler „Prozess läuft"-Test nicht aus. Ein Worker kann sauber auf HTTP antworten und trotzdem nutzlos sein – weil das Guthaben aufgebraucht ist, der Upstream nicht erreichbar ist oder die letzte erfolgreiche Lösung zehn Minuten zurückliegt.
Genau darin liegt der Unterschied zwischen „läuft noch" und „liefert noch brauchbare Arbeit". Wer beides über getrennte Endpunkte sichtbar macht, gibt Load Balancer und Kubernetes die Information, die sie zum Umleiten oder Neustarten brauchen – bevor fehlgeschlagene Aufgaben bei Ihren Nutzern landen. Dieser Leitfaden zeigt drei Endpunkte, die genau diese Trennung abbilden: /health/live, /health/ready und /health/dependencies, jeweils in Python (Flask) und Node.js (Express), plus die passende Kubernetes-Konfiguration.
Liveness, Readiness und Dependency: die drei Check-Typen
Produktionsreife Worker exponieren nicht einen, sondern drei Endpunkte. Jeder beantwortet eine andere Frage und löst eine andere Reaktion aus. Diese Trennung ist der Kern des gesamten Musters – wer Liveness und Readiness in einen Endpunkt zusammenzieht, bekommt entweder Endlos-Neustarts oder Traffic auf tote Worker.
| Check-Typ | Frage | Reaktion im Fehlerfall |
|---|---|---|
| Liveness | Läuft der Prozess überhaupt noch? | Container neu starten |
| Readiness | Kann der Worker jetzt Arbeit annehmen? | Traffic-Weiterleitung stoppen |
| Dependency | Sind Upstream-Dienste erreichbar? | Kontrolliert degradieren |
Der wichtigste Punkt: Ein eingefrorener Prozess schlägt bei der Liveness-Probe fehl und wird neu gestartet. Ein Worker mit leerem Guthaben läuft dagegen einwandfrei – er soll nur keine neuen Aufgaben mehr bekommen, bis das Guthaben aufgefüllt ist. Das ist ein Readiness-Fall, kein Neustart-Fall. Ein Neustart würde hier nichts lösen und im schlimmsten Fall eine Neustart-Schleife auslösen.
Welcher Check zählt in welchem Setup?
Nicht jede Deployment-Form braucht alle drei Endpunkte gleich dringend. Die folgende Zuordnung hilft bei der Priorisierung.
| Worker-Setup | Wichtigster Check | Warum |
|---|---|---|
| Einzelner Worker unter Supervisor (systemd, PM2) | Liveness + Readiness | Prozessfehler und nicht mehr nutzbare Worker sauber trennen |
| Kubernetes oder Load Balancer | Liveness + Readiness + Dependency | Routing und Neustarts hängen direkt an dieser Trennung |
| Queue-basierte Batch-Worker | Readiness vor Liveness | Der Prozess läuft, nimmt aber womöglich keine neuen Aufgaben mehr an |
Python: Health-Endpunkte mit Flask
Die Flask-Variante hält die Health-Metriken in einer thread-sicheren WorkerHealth-Datenklasse. Der Worker-Loop meldet nach jeder Aufgabe record_success() oder record_failure(); die drei Routen lesen diesen Zustand nur aus. Wichtig sind drei Details: Die Liveness-Route macht keinen einzigen externen Aufruf und antwortet dadurch sofort. Das Guthaben (getbalance) wird 60 Sekunden zwischengespeichert, damit nicht jede Probe eine API-Anfrage auslöst. Und die Readiness-Route liefert bei einem Problem den HTTP-Code 503 statt 200 – erst das veranlasst Kubernetes, den Worker aus dem Routing zu nehmen.
import requests
import time
import threading
from flask import Flask, jsonify
from dataclasses import dataclass, field
API_KEY = "YOUR_API_KEY"
RESULT_URL = "https://ocr.captchaai.com/res.php"
app = Flask(__name__)
@dataclass
class WorkerHealth:
"""Tracks worker health metrics."""
started_at: float = field(default_factory=time.monotonic)
last_solve_at: float = 0.0
total_solved: int = 0
total_failed: int = 0
consecutive_failures: int = 0
balance: float | None = None
balance_checked_at: float = 0.0
_lock: threading.Lock = field(default_factory=threading.Lock)
def record_success(self):
with self._lock:
self.total_solved += 1
self.last_solve_at = time.monotonic()
self.consecutive_failures = 0
def record_failure(self):
with self._lock:
self.total_failed += 1
self.consecutive_failures += 1
@property
def success_rate(self) -> float:
total = self.total_solved + self.total_failed
return self.total_solved / total if total > 0 else 1.0
@property
def seconds_since_last_solve(self) -> float:
if self.last_solve_at == 0:
return time.monotonic() - self.started_at
return time.monotonic() - self.last_solve_at
health = WorkerHealth()
# Thresholds
MAX_CONSECUTIVE_FAILURES = 10
MAX_SECONDS_WITHOUT_SOLVE = 600 # 10 minutes
MIN_BALANCE = 1.0
def check_balance() -> float | None:
"""Check CaptchaAI balance."""
now = time.monotonic()
# Cache balance for 60 seconds
if health.balance is not None and now - health.balance_checked_at < 60:
return health.balance
try:
resp = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "getbalance", "json": 1,
}, timeout=10).json()
health.balance = float(resp.get("request", 0))
health.balance_checked_at = now
return health.balance
except Exception:
return health.balance # Return cached value on error
@app.route("/health/live")
def liveness():
"""Liveness probe — is the process responsive?"""
return jsonify({"status": "ok", "uptime_s": int(time.monotonic() - health.started_at)}), 200
@app.route("/health/ready")
def readiness():
"""Readiness probe — can the worker accept tasks?"""
issues = []
# Check consecutive failures
if health.consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
issues.append(f"consecutive_failures={health.consecutive_failures}")
# Check time since last solve
if health.total_solved > 0 and health.seconds_since_last_solve > MAX_SECONDS_WITHOUT_SOLVE:
issues.append(f"no_solve_for={int(health.seconds_since_last_solve)}s")
# Check balance
balance = check_balance()
if balance is not None and balance < MIN_BALANCE:
issues.append(f"low_balance=${balance:.2f}")
if issues:
return jsonify({
"status": "not_ready",
"issues": issues,
"stats": {
"solved": health.total_solved,
"failed": health.total_failed,
"success_rate": round(health.success_rate, 3),
},
}), 503
return jsonify({
"status": "ready",
"stats": {
"solved": health.total_solved,
"failed": health.total_failed,
"success_rate": round(health.success_rate, 3),
"balance": balance,
},
}), 200
@app.route("/health/dependencies")
def dependencies():
"""Check upstream dependencies."""
checks = {}
# CaptchaAI API reachability
try:
resp = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "getbalance", "json": 1,
}, timeout=10)
checks["captchaai_api"] = {
"status": "ok" if resp.status_code == 200 else "degraded",
"response_ms": int(resp.elapsed.total_seconds() * 1000),
}
except Exception as e:
checks["captchaai_api"] = {"status": "down", "error": str(e)}
all_ok = all(c["status"] == "ok" for c in checks.values())
return jsonify({
"status": "ok" if all_ok else "degraded",
"checks": checks,
}), 200 if all_ok else 503
# --- Worker loop (runs in background) ---
def worker_loop():
"""Simulated CAPTCHA solving worker."""
while True:
try:
# ... solve CAPTCHA logic ...
health.record_success()
except Exception:
health.record_failure()
time.sleep(1)
threading.Thread(target=worker_loop, daemon=True).start()
Node.js: Health-Endpunkte mit Express
Wer den Worker-Stack in Node.js betreibt, bildet dasselbe Muster mit Express ab. Die Logik ist identisch: /health/live antwortet ohne externen Aufruf, /health/ready prüft aufeinanderfolgende Fehler, Zeit seit der letzten Lösung und Guthaben, /health/dependencies misst zusätzlich die Antwortzeit der CaptchaAI-API. Auch hier gilt die 60-Sekunden-Cache-Regel für das Guthaben und der 503-Code als Signal an den Load Balancer.
const express = require("express");
const API_KEY = "YOUR_API_KEY";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
const app = express();
const health = {
startedAt: Date.now(),
lastSolveAt: 0,
totalSolved: 0,
totalFailed: 0,
consecutiveFailures: 0,
balance: null,
balanceCheckedAt: 0,
recordSuccess() {
this.totalSolved++;
this.lastSolveAt = Date.now();
this.consecutiveFailures = 0;
},
recordFailure() {
this.totalFailed++;
this.consecutiveFailures++;
},
get successRate() {
const total = this.totalSolved + this.totalFailed;
return total > 0 ? this.totalSolved / total : 1;
},
};
async function checkBalance() {
if (health.balance !== null && Date.now() - health.balanceCheckedAt < 60000) {
return health.balance;
}
try {
const url = `${RESULT_URL}?key=${API_KEY}&action=getbalance&json=1`;
const resp = await (await fetch(url)).json();
health.balance = parseFloat(resp.request);
health.balanceCheckedAt = Date.now();
return health.balance;
} catch {
return health.balance;
}
}
app.get("/health/live", (req, res) => {
res.json({ status: "ok", uptimeMs: Date.now() - health.startedAt });
});
app.get("/health/ready", async (req, res) => {
const issues = [];
if (health.consecutiveFailures >= 10) {
issues.push(`consecutive_failures=${health.consecutiveFailures}`);
}
if (health.totalSolved > 0) {
const silentMs = Date.now() - health.lastSolveAt;
if (silentMs > 600_000) {
issues.push(`no_solve_for=${Math.round(silentMs / 1000)}s`);
}
}
const balance = await checkBalance();
if (balance !== null && balance < 1.0) {
issues.push(`low_balance=$${balance.toFixed(2)}`);
}
const stats = {
solved: health.totalSolved,
failed: health.totalFailed,
successRate: Math.round(health.successRate * 1000) / 1000,
balance,
};
if (issues.length > 0) {
return res.status(503).json({ status: "not_ready", issues, stats });
}
res.json({ status: "ready", stats });
});
app.get("/health/dependencies", async (req, res) => {
const checks = {};
try {
const start = Date.now();
const url = `${RESULT_URL}?key=${API_KEY}&action=getbalance&json=1`;
const resp = await fetch(url);
checks.captchaaiApi = {
status: resp.ok ? "ok" : "degraded",
responseMs: Date.now() - start,
};
} catch (e) {
checks.captchaaiApi = { status: "down", error: e.message };
}
const allOk = Object.values(checks).every((c) => c.status === "ok");
res.status(allOk ? 200 : 503).json({
status: allOk ? "ok" : "degraded",
checks,
});
});
app.listen(8080, () => console.log("Health server on :8080"));
Kubernetes: Liveness- und Readiness-Probes konfigurieren
In der Deployment-Spezifikation verweisen livenessProbe und readinessProbe auf die beiden HTTP-Pfade. Entscheidend sind die Zeitparameter: initialDelaySeconds gibt dem Prozess Zeit zum Hochfahren, periodSeconds steuert das Prüfintervall, failureThreshold legt fest, wie viele Fehlversuche in Folge nötig sind, bevor Kubernetes reagiert. Die Liveness-Probe darf großzügiger eingestellt sein (längeres Intervall, höherer Threshold) – ein zu strenger Wert löst unnötige Neustarts aus.
Ein praxisnahes Beispiel aus der DACH-Region: drei Worker-Pods auf einem verwalteten Kubernetes-Cluster bei Hetzner Cloud, davor ein Load Balancer. Fällt bei einem Pod das Guthaben unter die Schwelle, meldet dessen /health/ready einen 503; Kubernetes nimmt den Pod aus dem Service-Endpoint, die beiden anderen tragen die Last weiter. Sobald das Guthaben nachgeladen ist, wechselt die Probe zurück auf 200 und der Pod erhält wieder Traffic – ohne manuellen Eingriff.
apiVersion: apps/v1
kind: Deployment
metadata:
name: captcha-worker
spec:
replicas: 3
template:
spec:
containers:
- name: worker
image: captcha-worker:latest
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 2
HTTP-Statuscodes der Health-Endpunkte
Die gesamte Steuerung läuft über zwei Statuscodes: 200 heißt „alles in Ordnung", 503 heißt „bitte pausieren". Diese Semantik verstehen Kubernetes und praktisch jeder Load Balancer ohne weitere Konfiguration.
| Endpunkt | 200 | 503 |
|---|---|---|
/health/live |
Prozess reagiert | Prozess eingefroren – neu starten |
/health/ready |
Kann Aufgaben annehmen | Keine Aufgaben mehr senden |
/health/dependencies |
Alle Abhängigkeiten erreichbar | Upstream degradiert |
Typische Fehler und ihre Ursachen
Die meisten Health-Check-Probleme entstehen nicht am Worker selbst, sondern an falsch gesetzten Schwellen und Intervallen. Diese fünf Muster decken den Großteil der Support-Anfragen ab.
| Problem | Ursache | Lösung |
|---|---|---|
| Worker wird ständig neu gestartet | Liveness-Schwelle zu streng gesetzt | failureThreshold oder periodSeconds erhöhen |
| Worker beim Start als „not ready" markiert | Noch keine Lösung – wird fälschlich als „zu lange inaktiv" gewertet | seconds_since_last_solve erst nach der ersten Lösung prüfen |
| Health-Endpunkt wird langsam | API-Aufruf bei jeder Anfrage | Guthaben mit TTL zwischenspeichern (60 Sekunden empfohlen) |
| Health-Endpunkt stürzt selbst ab | Nicht abgefangene Ausnahme im Check | Jeden Check in try/except kapseln und degraded statt 500 zurückgeben |
| Falsch-negative Dependency-Prüfung | Kurzer Netzwerkaussetzer beim Guthaben-Abruf | Zwischengespeicherten Wert nutzen (stale-while-revalidate) |
Häufige Fragen
Wie oft sollte Kubernetes die Health-Endpunkte abfragen?
Liveness alle 10–30 Sekunden mit failureThreshold: 3, Readiness alle 5–10 Sekunden mit failureThreshold: 2. Häufigere Probes erkennen Probleme schneller, erzeugen aber mehr Last – für CAPTCHA-Worker ist dieser Overhead in der Regel vernachlässigbar.
Kann ich Liveness und Readiness über denselben Endpunkt abbilden?
Besser nicht. Werden beide Prüfungen zusammengezogen, führt jeder Readiness-Fehler – etwa leeres Guthaben – zu einem Neustart, obwohl der Prozess einwandfrei läuft. Halten Sie /health/live frei von externen Aufrufen und dadurch schnell; Guthaben- und Erreichbarkeitsprüfungen gehören in /health/ready und /health/dependencies. Nur mit getrennten Endpunkten trennt Kubernetes Neustart und Traffic-Umleitung sauber.
Warum wird das Guthaben zwischengespeichert?
Weil getbalance ein Netzwerkaufruf ist. Ohne Cache würde jede Readiness-Probe – bei kurzen Intervallen mehrmals pro Minute und pro Worker – eine eigene API-Anfrage auslösen. Ein Cache von 60 Sekunden hält die Angabe aktuell genug und entlastet zugleich den Endpunkt.
Was bedeutet ein niedriges Guthaben für die Readiness?
Fällt das Guthaben unter die gesetzte Schwelle (MIN_BALANCE), meldet /health/ready einen 503, und der Worker bekommt keine neuen Aufgaben mehr. Da CaptchaAI Thread-basiert abrechnet – jeder Tarif ab BASIC (15 $/Monat, 5 Threads) enthält unbegrenzte Lösungen pro Thread –, geht es hier um das verbleibende Guthaben, nicht um ein Lösungslimit. Nach dem Aufladen wechselt die Probe automatisch zurück auf 200.
Wie überwache ich die Health-Werte über viele Worker hinweg?
Exponieren Sie neben den Health-Endpunkten eine /metrics-Route im Prometheus-Format und aggregieren Sie die Werte in Grafana. So sehen Sie Erfolgsquote, Guthaben und Inaktivität flottenweit statt pro Pod.
Verwandte Leitfäden
- Horizontale Skalierung für CAPTCHA-Worker
- CaptchaAI-Metriken mit Prometheus und Grafana überwachen
- Fehlerbudget-Tracking für CAPTCHA-Zuverlässigkeit