Tutorials

Server-Sent Events: CAPTCHA-Lösungen in Echtzeit empfangen

Ein Live-Dashboard soll jede gelöste CAPTCHA-Abfrage sofort anzeigen – ohne dass der Browser den Server im Sekundentakt fragt, ob schon etwas fertig ist. Server-Sent Events (SSE) liefern genau dieses Verhalten: eine einzige dauerhafte HTTP-Verbindung, über die Ihr Server jede Lösung in dem Moment weiterreicht, in dem der Callback von CaptchaAI eintrifft. Kein Polling von res.php, keine überflüssigen Anfragen, Zustellung in Sekundenbruchteilen. Dieser Leitfaden zeigt die komplette Kette – von der Aufgaben-Übermittlung bis zum Event im Browser – mit Flask und Express.

Voraussetzungen

Bevor Sie loslegen, sollten drei Dinge bereitstehen:

  • ein CaptchaAI-API-Schlüssel (kostenlos über captchaai.com erhältlich);
  • ein öffentlich erreichbarer Endpunkt, den CaptchaAI als pingback aufrufen kann – lokal genügt ein Tunnel wie ngrok oder Cloudflare Tunnel;
  • ein Browser mit EventSource, das jeder aktuelle Browser mitbringt.

Wann SSE die richtige Wahl ist

Bevor Sie Code schreiben, lohnt der Blick auf die Alternativen. CAPTCHA-Ergebnisse fließen ausschließlich in eine Richtung: vom Server zum Client. Genau dafür ist SSE gebaut. Eine bidirektionale WebSocket-Verbindung wäre überdimensioniert, klassisches Polling verschwendet Anfragen und erhöht die Latenz.

Merkmal SSE WebSocket Polling
Richtung Server → Client bidirektional Client → Server
Protokoll HTTP/1.1+ WS/WSS HTTP
Auto-Reconnect eingebaut manuell entfällt
Browser-Support alle modernen alle modernen alle
Komplexität niedrig mittel niedrig
Überflüssige Anfragen keine keine viele
Für CAPTCHA-Ergebnisse ideal überdimensioniert funktioniert, aber verschwenderisch

Für ein Dashboard, das gelöste CAPTCHAs einer Scraping-Flotte in Echtzeit mitschreibt, ist SSE damit die schlankste Option: Der Browser übernimmt über das native EventSource-Objekt automatisch die Wiederverbindung, und Sie kommen ohne zusätzliche Bibliothek aus.

So greift SSE in den CAPTCHA-Workflow

An der Kette sind drei Parteien beteiligt: der Browser-Client, Ihr eigener Server und die CaptchaAI-API. Der pingback-Parameter verbindet sie – er sagt CaptchaAI, an welche URL das fertige Ergebnis geschickt werden soll.

[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
   ↓                          ↑
   Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
  1. Der Client öffnet eine dauerhafte Verbindung zu Ihrem SSE-Endpunkt (persistente HTTP-Verbindung).
  2. Der Client übermittelt eine CAPTCHA-Aufgabe an CaptchaAI und setzt pingback auf die Callback-URL Ihres Servers.
  3. CaptchaAI löst das CAPTCHA und ruft Ihren Callback-Endpunkt mit dem Ergebnis auf.
  4. Ihr Server schiebt das Ergebnis durch den offenen SSE-Stream an den Client.

Hinweis: Der pingback-Endpunkt muss von außen erreichbar sein. In der lokalen Entwicklung leiten Sie ihn über einen Tunnel nach draußen; im Betrieb ist es eine feste Route auf Ihrem Server.

Vollständige Implementierung – Python (Flask)

Server

Der Flask-Server hält pro Client eine eigene Queue vor. Der SSE-Endpunkt blockiert auf dieser Queue und sendet alle 30 Sekunden einen Keepalive-Kommentar, damit Reverse Proxies die Verbindung nicht schließen. Trifft der Callback von CaptchaAI ein, legt der /callback-Handler das Ergebnis in die passende Queue – und der offene Stream reicht es sofort weiter.

import os
import queue
import threading
import requests
from flask import Flask, Response, request, jsonify

app = Flask(__name__)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Per-client event queues: client_id -> Queue
client_queues = {}
queues_lock = threading.Lock()


@app.route("/events/<client_id>")
def sse_stream(client_id):
    """SSE endpoint — clients connect here for real-time results."""
    q = queue.Queue()

    with queues_lock:
        client_queues[client_id] = q

    def generate():
        try:
            while True:
                # Block until a result arrives (timeout for keepalive)
                try:
                    data = q.get(timeout=30)
                    yield f"event: captcha-solved\ndata: {data}\n\n"
                except queue.Empty:
                    # Send keepalive comment to prevent connection timeout
                    yield ": keepalive\n\n"
        finally:
            with queues_lock:
                client_queues.pop(client_id, None)

    return Response(
        generate(),
        mimetype="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no"  # Disable nginx buffering
        }
    )


@app.route("/submit", methods=["POST"])
def submit_captcha():
    """Submit a CAPTCHA task with callback to this server."""
    data = request.json
    client_id = data["client_id"]
    sitekey = data["sitekey"]
    pageurl = data["pageurl"]

    callback_url = f"{request.host_url}callback?client_id={client_id}"

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": callback_url,
        "json": 1
    })
    result = resp.json()

    if result.get("status") == 1:
        return jsonify({"task_id": result["request"]})
    return jsonify({"error": result.get("request")}), 400


@app.route("/callback")
def captcha_callback():
    """Receive CaptchaAI callback and push to SSE stream."""
    client_id = request.args.get("client_id")
    task_id = request.args.get("id")
    solution = request.args.get("code")

    import json
    message = json.dumps({
        "task_id": task_id,
        "solution": solution
    })

    with queues_lock:
        q = client_queues.get(client_id)
        if q:
            q.put(message)

    return "OK", 200


if __name__ == "__main__":
    app.run(port=5000, threaded=True)

Browser-Client

Auf der Client-Seite genügt das native EventSource-Objekt. Es abonniert das benannte Event captcha-solved und rendert jedes eintreffende Ergebnis, sobald es ankommt – ganz ohne Polling-Schleife.

<!DOCTYPE html>
<html>
<body>
  <button onclick="submitCaptcha()">Solve CAPTCHA</button>
  <div id="results"></div>

  <script>
    const clientId = crypto.randomUUID();
    const resultsDiv = document.getElementById("results");

    // Connect SSE stream
    const eventSource = new EventSource(`/events/${clientId}`);

    eventSource.addEventListener("captcha-solved", (event) => {
      const data = JSON.parse(event.data);
      resultsDiv.innerHTML += `<p>Task ${data.task_id}: ${data.solution.substring(0, 30)}...</p>`;
    });

    eventSource.onerror = () => {
      console.log("SSE connection lost, reconnecting...");
    };

    async function submitCaptcha() {
      const response = await fetch("/submit", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          client_id: clientId,
          sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
          pageurl: "https://example.com"
        })
      });
      const result = await response.json();
      resultsDiv.innerHTML += `<p>Submitted: ${result.task_id}</p>`;
    }
  </script>
</body>
</html>

Vollständige Implementierung – JavaScript (Express)

Server

Die Express-Variante folgt derselben Logik, hält die offenen Antworten aber direkt in einer Map statt in Queues. Node.js eignet sich hier besonders gut, denn jede SSE-Verbindung ist nur eine leichtgewichtige, offen gehaltene HTTP-Antwort.

const express = require("express");
const axios = require("axios");

const app = express();
app.use(express.json());

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";

// Per-client SSE connections: clientId -> Response object
const clients = new Map();

// SSE endpoint
app.get("/events/:clientId", (req, res) => {
  const clientId = req.params.clientId;

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "X-Accel-Buffering": "no",
  });

  clients.set(clientId, res);

  // Keepalive every 30 seconds
  const keepalive = setInterval(() => {
    res.write(": keepalive\n\n");
  }, 30000);

  req.on("close", () => {
    clearInterval(keepalive);
    clients.delete(clientId);
  });
});

// Submit CAPTCHA
app.post("/submit", async (req, res) => {
  const { client_id, sitekey, pageurl } = req.body;
  const callbackUrl = `${BASE_URL}/callback?client_id=${client_id}`;

  try {
    const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
      params: {
        key: API_KEY,
        method: "userrecaptcha",
        googlekey: sitekey,
        pageurl: pageurl,
        pingback: callbackUrl,
        json: 1,
      },
    });

    if (resp.data.status === 1) {
      return res.json({ task_id: resp.data.request });
    }
    res.status(400).json({ error: resp.data.request });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// CaptchaAI callback → push to SSE
app.get("/callback", (req, res) => {
  const clientId = req.query.client_id;
  const taskId = req.query.id;
  const solution = req.query.code;

  const clientRes = clients.get(clientId);
  if (clientRes) {
    const data = JSON.stringify({ task_id: taskId, solution: solution });
    clientRes.write(`event: captcha-solved\ndata: ${data}\n\n`);
  }

  res.sendStatus(200);
});

app.listen(3000, () => console.log("SSE server running on :3000"));

Produktion: Skalierung und Grenzen

Mehrere Server-Instanzen

SSE-Verbindungen sind zustandsbehaftet. Läuft Ihr Server hinter einem Load Balancer über mehrere Instanzen, kann der Callback eine andere Instanz treffen als die, die den SSE-Stream des Clients hält – das Ergebnis käme nie an. Ein gemeinsamer Nachrichtenbus löst das Problem. Mit Redis Pub/Sub veröffentlicht der Callback-Handler das Ergebnis auf einem Kanal, den der SSE-Handler abonniert hat:

# Callback handler publishes to Redis
import redis
r = redis.Redis()
r.publish(f"captcha:{client_id}", json.dumps(message))

# SSE handler subscribes to Redis
pubsub = r.pubsub()
pubsub.subscribe(f"captcha:{client_id}")
for msg in pubsub.listen():
    if msg["type"] == "message":
        yield f"data: {msg['data'].decode()}\n\n"

Dieses Muster ist gerade beim Deployment auf Hetzner oder IONOS relevant: Sobald mehr als eine Instanz hinter nginx läuft, ist der geteilte Bus Pflicht – sonst verschwinden einzelne Ergebnisse scheinbar zufällig.

Verbindungslimits im Browser

Über HTTP/1.1 erlauben Browser nur 6 gleichzeitige SSE-Verbindungen pro Domain. Setzen Sie auf HTTP/2, um dieses Limit deutlich anzuheben, oder bündeln Sie mehrere Aufgaben-Ergebnisse über eine einzige SSE-Verbindung pro Client. Für ein Ops-Dashboard, das Dutzende parallele Solves abbildet, ist die zweite Variante meist die robustere.

Fehlerbehebung

Problem Ursache Lösung
SSE-Verbindung bricht alle 30 s ab Timeout im Reverse Proxy oder Load Balancer Keepalive-Kommentare senden; Proxy-Timeout über das Keepalive-Intervall heben
Ergebnisse erreichen den Client nicht Callback landet auf einer anderen Server-Instanz Redis Pub/Sub zwischen Callback- und SSE-Handler schalten
Fehler in der Browser-Konsole fehlende CORS-Header Header Access-Control-Allow-Origin am SSE-Endpunkt setzen
Ständige Reconnects fehlerhaft formatierte SSE-Nachrichten Jedes Event mit \n\n abschließen und das Datenformat prüfen

Häufige Fragen

Worin unterscheidet sich SSE von einem reinen Webhook?

Ein Webhook liefert das Ergebnis an Ihren Server, SSE reicht es von dort an den Browser weiter. Beide arbeiten hier zusammen: Der pingback-Callback von CaptchaAI ist der Webhook, der Ihren Server erreicht; der SSE-Stream ist die letzte Meile bis zum Nutzer. Ohne SSE müssten Sie den Browser weiterhin pollen lassen.

Was passiert, wenn die SSE-Verbindung abbricht?

Der Browser verbindet sich automatisch neu. Das EventSource-Objekt startet nach einem Abbruch von selbst einen neuen Versuch – anders als bei WebSocket müssen Sie diese Logik nicht selbst schreiben. Halten Sie serverseitig die Keepalive-Kommentare aktiv, damit Reverse Proxies die Verbindung nicht vorzeitig kappen.

Funktioniert SSE hinter nginx oder einem Reverse Proxy?

Ja, sofern Sie das Response-Buffering abschalten. nginx puffert Antworten standardmäßig; der Header X-Accel-Buffering: no sorgt dafür, dass jedes Event sofort durchgereicht wird. Zusätzlich sollte das Proxy-Timeout über dem Keepalive-Intervall liegen, sonst bricht die Verbindung regelmäßig ab.

Kann ich Ergebnisse mehrerer CAPTCHA-Typen über eine Verbindung streamen?

Ja. Der SSE-Stream transportiert beliebige Ergebnisse – ob reCAPTCHA v2, reCAPTCHA v3 oder Cloudflare Turnstile. Übermitteln Sie jede Aufgabe mit derselben client_id, und alle Lösungen laufen über dieselbe Verbindung ein. Die Beispiele nutzen userrecaptcha; für andere Typen tauschen Sie lediglich die Aufgaben-Parameter aus.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.