DevOps & Skalierung

CaptchaAI hinter einem Load Balancer: Architekturmuster

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

Kommentare sind für diesen Artikel deaktiviert.