Sobald Ihre Automatisierung mehrere tausend CAPTCHA-Anfragen pro Stunde stellt, entscheidet nicht mehr die reine Lösungsgeschwindigkeit über den Durchsatz, sondern die Architektur davor. Die belastbare Antwort ist ein Pool gleichartiger Worker hinter einem Load Balancer: Er verteilt jede Anfrage an eine freie Instanz, überspringt ausgefallene Worker und wächst horizontal mit. Dieser Leitfaden zeigt die passenden Routing-Muster – von NGINX über gewichtete Verteilung bis zum clientseitigen Load-Balancing – und warum Least Connections bei stark schwankenden Lösungszeiten die richtige Wahl ist.
Ein einzelner Worker-Prozess ist schnell ausgereizt: Jede offene CAPTCHA-Aufgabe belegt eine Verbindung für 5 bis 120 Sekunden. Verteilen Sie diese Last auf drei, fünf oder zwanzig Worker, steigt der Durchsatz nahezu linear – vorausgesetzt, das Routing berücksichtigt die tatsächliche Auslastung jeder Instanz.
Architektur im Überblick
[Scraper 1] ──┐ ┌── [Worker 1] ──→ CaptchaAI API
[Scraper 2] ──┤── [Load Balancer] ──┤── [Worker 2] ──→ CaptchaAI API
[Scraper 3] ──┘ └── [Worker 3] ──→ CaptchaAI API
Jeder Scraper spricht nur den Load Balancer an. Dahinter nehmen die Worker die Anfragen entgegen, übermitteln sie an die CaptchaAI-API und fragen das Ergebnis per Polling ab. Fällt ein Worker aus, leitet der Load Balancer den Verkehr auf die verbleibenden Instanzen um – ohne dass der aufrufende Scraper etwas davon merkt.
NGINX als Load Balancer konfigurieren
NGINX ist im DACH-Raum die verbreitetste Wahl für diese Zwischenschicht: schlank, gut dokumentiert und auf jeder Hetzner- oder IONOS-VM in wenigen Minuten aufgesetzt. Drei Muster decken die meisten Setups ab.
Round-Robin (Standardverfahren)
upstream captcha_workers {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
}
server {
listen 80;
server_name captcha.internal;
location /solve {
proxy_pass http://captcha_workers;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10s;
proxy_read_timeout 300s; # CAPTCHA solving can take minutes
}
location /health {
proxy_pass http://captcha_workers;
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
}
}
Round-Robin schickt die Anfragen reihum an jeden Worker. Das ist einfach und fair, solange jede Aufgabe ungefähr gleich lange dauert – beim CAPTCHA-Solving ist das selten der Fall. Beachten Sie das großzügige proxy_read_timeout von 300 Sekunden: Eine Lösung kann dauern, und ein zu knappes Timeout bricht laufende Aufgaben vorzeitig ab.
Least Connections – die bessere Wahl fürs CAPTCHA-Solving
upstream captcha_workers {
least_conn; # Route to worker with fewest active connections
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080 weight=2; # Higher capacity worker
# Health checks
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}
Mit least_conn geht jede neue Anfrage an den Worker mit den wenigsten aktiven Verbindungen. Genau das brauchen Sie, wenn ein Bild-CAPTCHA in wenigen Sekunden fertig ist, ein reCAPTCHA v2 aber deutlich länger dauert. Das Attribut weight=2 gibt einer leistungsfähigeren Instanz proportional mehr Last; max_fails in Kombination mit fail_timeout nimmt einen wiederholt fehlschlagenden Worker automatisch aus der Rotation.
Backup-Worker für Ausfälle
upstream captcha_workers {
least_conn;
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080 backup; # Only used when others are down
}
Ein als backup markierter Worker bleibt im Normalbetrieb ungenutzt und springt erst ein, wenn die regulären Instanzen nicht erreichbar sind. So halten Sie Reservekapazität vor, ohne im Alltag dafür Rechenleistung zu bezahlen.
Der Worker-API-Server
Jeder Worker ist ein kleiner HTTP-Dienst mit zwei Endpunkten: /solve nimmt die Aufgabe entgegen und spricht die CaptchaAI-API, /health meldet die aktuelle Auslastung an den Load Balancer. Entscheidend ist die Obergrenze MAX_CONCURRENT: Meldet ein Worker ab 90 % Auslastung den Status overloaded (HTTP 503), nimmt der Load Balancer ihn aus der Verteilung, bis wieder Kapazität frei ist.
Python (Flask)
import os
import time
import threading
import requests
from flask import Flask, request, jsonify
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
app = Flask(__name__)
# Track active tasks for load reporting
active_tasks = 0
tasks_lock = threading.Lock()
max_concurrent = int(os.environ.get("MAX_CONCURRENT", "20"))
@app.route("/solve", methods=["POST"])
def solve():
global active_tasks
with tasks_lock:
if active_tasks >= max_concurrent:
return jsonify({"error": "WORKER_AT_CAPACITY"}), 503
active_tasks += 1
try:
data = request.json
result = solve_captcha(data)
return jsonify(result)
finally:
with tasks_lock:
active_tasks -= 1
@app.route("/health")
def health():
with tasks_lock:
load = active_tasks / max_concurrent
return jsonify({
"status": "healthy" if load < 0.9 else "overloaded",
"active_tasks": active_tasks,
"max_concurrent": max_concurrent,
"load_pct": round(load * 100, 1)
}), 200 if load < 0.9 else 503
def solve_captcha(data):
session = requests.Session()
payload = {
"key": API_KEY,
"method": data.get("method", "userrecaptcha"),
"googlekey": data.get("sitekey"),
"pageurl": data.get("pageurl"),
"json": 1
}
if data.get("proxy"):
payload["proxy"] = data["proxy"]
payload["proxytype"] = data.get("proxytype", "HTTP")
resp = session.post("https://ocr.captchaai.com/in.php", data=payload)
result = resp.json()
if result.get("status") != 1:
return {"error": result.get("request")}
captcha_id = result["request"]
for _ in range(60):
time.sleep(5)
poll = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": captcha_id, "json": 1
}).json()
if poll.get("status") == 1:
return {"solution": poll["request"], "captcha_id": captcha_id}
if poll.get("request") != "CAPCHA_NOT_READY":
return {"error": poll.get("request")}
return {"error": "TIMEOUT"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, threaded=True)
JavaScript (Express)
const express = require("express");
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT || "20", 10);
const PORT = parseInt(process.env.PORT || "8080", 10);
let activeTasks = 0;
const app = express();
app.use(express.json());
app.post("/solve", async (req, res) => {
if (activeTasks >= MAX_CONCURRENT) {
return res.status(503).json({ error: "WORKER_AT_CAPACITY" });
}
activeTasks++;
try {
const result = await solveCaptcha(req.body);
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
activeTasks--;
}
});
app.get("/health", (req, res) => {
const load = activeTasks / MAX_CONCURRENT;
const status = load < 0.9 ? "healthy" : "overloaded";
res
.status(load < 0.9 ? 200 : 503)
.json({ status, activeTasks, maxConcurrent: MAX_CONCURRENT, loadPct: Math.round(load * 100) });
});
async function solveCaptcha(data) {
const submitResp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: data.method || "userrecaptcha",
googlekey: data.sitekey,
pageurl: data.pageurl,
json: 1,
},
});
if (submitResp.data.status !== 1) {
return { error: submitResp.data.request };
}
const captchaId = submitResp.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (pollResp.data.status === 1) {
return { solution: pollResp.data.request, captchaId };
}
if (pollResp.data.request !== "CAPCHA_NOT_READY") {
return { error: pollResp.data.request };
}
}
return { error: "TIMEOUT" };
}
app.listen(PORT, () => console.log(`Worker listening on port ${PORT}`));
Beide Varianten sind funktionsgleich: Sie zählen aktive Aufgaben mit, weisen bei Überlast mit 503 ab und geben über /health einen Lastwert zurück. Wählen Sie die Sprache, die zu Ihrem bestehenden Stack passt – die API-Aufrufe (in.php zum Übermitteln, res.php zum Abfragen) sind in beiden identisch.
Routing-Strategien im Vergleich
| Strategie | Funktionsweise | Am besten geeignet für |
|---|---|---|
| Round-Robin | Sequentielle Rotation | Worker mit gleicher Kapazität |
| Least Connections | Anfrage geht an den Worker mit den wenigsten aktiven Verbindungen | CAPTCHA-Solving (variable Aufgabendauer) |
| Gewichtet (Weighted) | Verteilung proportional zum Gewicht | Worker mit gemischter Kapazität |
| IP-Hash | Gleicher Client → gleicher Worker | wenn Sitzungsaffinität nötig ist |
| Zufällig (Random) | Zufällige Auswahl | einfache, gleichmäßig verteilte Last |
Empfehlung: Für CAPTCHA-Solving ist Least Connections die beste Wahl. Die Lösungszeit schwankt (5–120 Sekunden), sodass Round-Robin zwangsläufig zu ungleichmäßiger Last führt – ein Worker sammelt langsame Aufgaben an, während ein anderer bereits wieder leerläuft.
Worker-Parallelität an Ihr Thread-Kontingent koppeln
CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – ein Thread ist eine gerade in Bearbeitung befindliche CAPTCHA-Aufgabe, und innerhalb des Tarifs sind die Lösungen unbegrenzt. Für die Architektur heißt das: Die Summe der MAX_CONCURRENT-Werte über alle Worker sollte zum Thread-Kontingent Ihres Tarifs passen. Mit ADVANCE (90 $/Monat, 50 Threads) verteilen Sie beispielsweise 50 gleichzeitige Aufgaben auf Ihre Worker – etwa fünf Instanzen mit je 10 –, mit PREMIUM (170 $/Monat, 100 Threads) entsprechend mehr. Lassen Sie hingegen jeden Worker unbegrenzt Aufgaben annehmen, laufen Sie an die Thread-Grenze und ernten Fehler, statt den Durchsatz zu erhöhen.
Ein praxisnahes Setup im DACH-Raum: drei Worker-VMs bei Hetzner Cloud, davor ein NGINX-Load-Balancer auf einer vierten Instanz, ausgerollt über GitLab CI. Die Tarifpreise verstehen sich in US-Dollar.
Load-Balancing im Client (ohne externen Proxy)
Wenn Sie keinen externen Load Balancer betreiben möchten – etwa in einem kleinen Setup oder für lokale Tests – verlagern Sie das Routing direkt in den Client:
import random
import requests
class ClientLoadBalancer:
def __init__(self, workers):
self.workers = [
{"url": url, "healthy": True, "active": 0}
for url in workers
]
def get_worker(self):
healthy = [w for w in self.workers if w["healthy"]]
if not healthy:
raise Exception("No healthy workers")
return min(healthy, key=lambda w: w["active"])
def solve(self, task):
worker = self.get_worker()
worker["active"] += 1
try:
resp = requests.post(
f"{worker['url']}/solve",
json=task,
timeout=300
)
if resp.status_code == 503:
worker["healthy"] = False
return self.solve(task) # Retry on another worker
return resp.json()
except requests.RequestException:
worker["healthy"] = False
return self.solve(task)
finally:
worker["active"] -= 1
lb = ClientLoadBalancer([
"http://10.0.1.10:8080",
"http://10.0.1.11:8080",
"http://10.0.1.12:8080"
])
result = lb.solve({"sitekey": "6Le-wvkS...", "pageurl": "https://example.com"})
Der Client wählt jeweils den Worker mit den wenigsten aktiven Aufgaben, markiert eine überlastete oder nicht erreichbare Instanz als healthy=False und wiederholt die Aufgabe automatisch auf einem anderen Worker. Für zwei bis vier Worker reicht dieses Muster oft aus und spart eine zusätzliche Komponente im Betrieb.
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
Worker als healthy=False markiert |
Health-Check schlägt fehl | Erreichbarkeit des Workers prüfen, Timeout-Werte und Netzwerkpfade validieren |
| Alle Anfragen gehen an einen Worker | Round-Robin bei stark schwankender Aufgabendauer | Auf Least Connections umstellen, active-Zähler prüfen |
| Erhöhte Fehlerrate unter Last | Worker überlastet oder Thread-Kontingent erreicht | Parallelität pro Worker reduzieren, weitere Worker hinzufügen, Tarif-Threads prüfen |
| Failover wird nicht ausgelöst | Fehlerbehandlung im Wrapper fehlt | try/except um die API-Aufrufe legen und den Health-Status setzen |
| Zeitüberschreitung bei langen Lösungen | proxy_read_timeout zu knapp |
Auf 300 Sekunden oder mehr erhöhen |
FAQ
Wie viele Worker brauche ich für meinen Durchsatz?
Rechnen Sie von Ihrem Thread-Kontingent rückwärts. Jeder gleichzeitige Thread entspricht einer offenen Aufgabe von 5 bis 120 Sekunden; teilen Sie das Kontingent auf so viele Worker auf, dass keiner dauerhaft an seiner MAX_CONCURRENT-Grenze arbeitet. Fünf Worker mit je 10 parallelen Aufgaben decken die 50 Threads von ADVANCE (90 $/Monat) sauber ab.
Warum ist Least Connections besser als Round-Robin?
Weil die Lösungszeit stark schwankt. Round-Robin verteilt stur reihum und schickt einem Worker, der gerade an mehreren langsamen reCAPTCHA-Aufgaben hängt, trotzdem die nächste Anfrage. Least Connections wählt dagegen immer die Instanz mit den wenigsten aktiven Verbindungen und gleicht die Last dadurch von selbst aus.
Wie passt die Worker-Parallelität zu meinem CaptchaAI-Tarif?
Die Summe aller MAX_CONCURRENT-Werte sollte das Thread-Kontingent Ihres Tarifs nicht überschreiten. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – setzen Sie die Obergrenzen der Worker zu hoch, laufen Sie an die Thread-Grenze und erhalten Fehler statt mehr Durchsatz.
Was passiert, wenn ein Worker mitten in einer Lösung ausfällt?
Der Load Balancer erkennt den Ausfall über den Health-Check und leitet neue Anfragen auf die verbleibenden Worker um. Beim clientseitigen Muster markiert der Client die Instanz als healthy=False und wiederholt die betroffene Aufgabe automatisch auf einem anderen Worker.
Sollte ich Sticky Sessions aktivieren?
Nein. CAPTCHA-Anfragen sind zustandslos – jeder Worker kann jede Aufgabe bearbeiten. Sticky Sessions würden nur zu ungleichmäßiger Last führen, ohne einen Vorteil zu bringen.
Verwandte Leitfäden
- Architekturmuster für CAPTCHA-Solving bei hohem Volumen
- Worker für CAPTCHA-Solving horizontal skalieren
- Health-Check-Endpunkte für CAPTCHA-Worker