Ein Event Bus ist die richtige Architektur, sobald mehrere Teile Ihrer Anwendung auf denselben CAPTCHA-Vorgang reagieren müssen – Logging, Metriken und Wiederholungslogik, ohne dass eine Komponente die andere kennt. Statt nur ein Ergebnis zurückzugeben, sendet der Bus jeden Zustand im Lebenszyklus als Ereignis: übermittelt, ausstehend, gelöst, fehlgeschlagen, Timeout.
Rückrufe und reines Polling liefern zwar das Endergebnis, geben Ihnen aber keinen Einblick in die Zwischenzustände und koppeln die auswertende Logik eng an den Aufruf. Genau diese Lücke schließt ein EventEmitter-basierter Bus in Node.js: Er entkoppelt das Lösen vom Reagieren und macht den gesamten Ablauf beobachtbar.
Die Architektur im Überblick
[CaptchaBus]
├── emit("submitted", { taskId, type, pageurl })
├── emit("pending", { taskId, elapsed })
├── emit("solved", { taskId, solution, duration })
├── emit("failed", { taskId, error, duration })
└── emit("timeout", { taskId, elapsed })
↓ ↓ ↓
[Logger] [Metrics] [Retry Handler]
Jeder Listener registriert sich eigenständig auf die Ereignisse, die ihn interessieren. Eine neue Funktion – etwa das Sammeln von Metriken – hängt sich einfach an das passende Ereignis an, ganz ohne Eingriff in den Code, der die CAPTCHAs tatsächlich löst. So bleibt die Kernlogik schlank, während Beobachtbarkeit und Nebenfunktionen frei wachsen können.
Die CaptchaBus-Klasse in JavaScript
Die Klasse erweitert den eingebauten EventEmitter von Node.js. Die Methode submit reicht die Aufgabe bei CaptchaAI ein, _poll fragt das Ergebnis in festen Intervallen ab, und an jedem Übergang wird das entsprechende Ereignis gesendet. Der Aufrufer erhält sofort eine taskId zurück und wartet nirgends blockierend.
const EventEmitter = require("events");
const axios = require("axios");
class CaptchaBus extends EventEmitter {
constructor(apiKey, options = {}) {
super();
this.apiKey = apiKey;
this.pollInterval = options.pollInterval || 5000;
this.maxWait = options.maxWait || 300000; // 5 minutes
this.pending = new Map();
}
async submit(params) {
const { method, sitekey, pageurl, ...extra } = params;
const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const submitParams = {
key: this.apiKey,
method: method || "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: 1,
...extra,
};
try {
const resp = await axios.post(
"https://ocr.captchaai.com/in.php",
null,
{ params: submitParams }
);
if (resp.data.status !== 1) {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: 0,
});
return null;
}
const captchaId = resp.data.request;
const startTime = Date.now();
this.emit("submitted", {
taskId,
captchaId,
method: method || "userrecaptcha",
pageurl,
});
// Start polling
this._poll(taskId, captchaId, startTime);
return taskId;
} catch (err) {
this.emit("failed", { taskId, error: err.message, duration: 0 });
return null;
}
}
async _poll(taskId, captchaId, startTime) {
const check = async () => {
const elapsed = Date.now() - startTime;
if (elapsed > this.maxWait) {
this.emit("timeout", { taskId, elapsed });
return;
}
this.emit("pending", { taskId, elapsed });
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: {
key: this.apiKey,
action: "get",
id: captchaId,
json: 1,
},
});
if (resp.data.status === 1) {
this.emit("solved", {
taskId,
captchaId,
solution: resp.data.request,
duration: Date.now() - startTime,
});
} else if (resp.data.request === "CAPCHA_NOT_READY") {
setTimeout(check, this.pollInterval);
} else {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: Date.now() - startTime,
});
}
} catch (err) {
this.emit("failed", {
taskId,
error: err.message,
duration: Date.now() - startTime,
});
}
};
setTimeout(check, this.pollInterval);
}
}
module.exports = CaptchaBus;
Listener registrieren: Logging und Metriken
Weil der Bus ein EventEmitter ist, hängen Sie beliebig viele Listener an dasselbe Ereignis. Hier läuft ein Listener für lesbare Konsolenausgaben und ein zweiter, der parallel Kennzahlen zusammenzählt – beide voneinander unabhängig. Der Zähler für Metriken weiß nichts vom Logging und umgekehrt.
const CaptchaBus = require("./captcha-bus");
const bus = new CaptchaBus(process.env.CAPTCHAAI_API_KEY, {
pollInterval: 5000,
maxWait: 120000,
});
// Logging listener
bus.on("submitted", (e) => {
console.log(`[SUBMIT] ${e.taskId} → ${e.method} on ${e.pageurl}`);
});
bus.on("pending", (e) => {
console.log(`[PENDING] ${e.taskId} — ${(e.elapsed / 1000).toFixed(1)}s`);
});
bus.on("solved", (e) => {
console.log(
`[SOLVED] ${e.taskId} in ${(e.duration / 1000).toFixed(1)}s — ${e.solution.substring(0, 30)}...`
);
});
bus.on("failed", (e) => {
console.error(`[FAILED] ${e.taskId} — ${e.error}`);
});
bus.on("timeout", (e) => {
console.error(
`[TIMEOUT] ${e.taskId} after ${(e.elapsed / 1000).toFixed(1)}s`
);
});
// Metrics listener
const metrics = { submitted: 0, solved: 0, failed: 0, totalDuration: 0 };
bus.on("submitted", () => metrics.submitted++);
bus.on("solved", (e) => {
metrics.solved++;
metrics.totalDuration += e.duration;
});
bus.on("failed", () => metrics.failed++);
// Submit a CAPTCHA
bus.submit({
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
Dasselbe Muster in Python
Python bringt keinen EventEmitter mit, das Muster lässt sich aber mit einem defaultdict aus Listener-Listen in wenigen Zeilen nachbauen. Das Polling läuft hier in einem Hintergrund-Thread, damit submit genauso nicht-blockierend zurückkehrt wie in der Node.js-Variante.
import os
import time
import threading
from collections import defaultdict
import requests
class CaptchaBus:
def __init__(self, api_key, poll_interval=5, max_wait=300):
self.api_key = api_key
self.poll_interval = poll_interval
self.max_wait = max_wait
self._listeners = defaultdict(list)
def on(self, event, callback):
"""Register a listener for an event."""
self._listeners[event].append(callback)
return self
def emit(self, event, data):
"""Emit an event to all registered listeners."""
for callback in self._listeners.get(event, []):
try:
callback(data)
except Exception as e:
print(f"Listener error on {event}: {e}")
def submit(self, sitekey, pageurl, method="userrecaptcha", **extra):
"""Submit a CAPTCHA and begin tracking."""
task_id = f"task_{int(time.time())}_{id(sitekey) % 10000}"
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": method,
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
**extra
})
data = resp.json()
if data.get("status") != 1:
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": 0
})
return None
captcha_id = data["request"]
start_time = time.time()
self.emit("submitted", {
"task_id": task_id,
"captcha_id": captcha_id,
"method": method,
"pageurl": pageurl
})
# Poll in a background thread
thread = threading.Thread(
target=self._poll,
args=(task_id, captcha_id, start_time),
daemon=True
)
thread.start()
return task_id
def _poll(self, task_id, captcha_id, start_time):
while True:
elapsed = time.time() - start_time
if elapsed > self.max_wait:
self.emit("timeout", {"task_id": task_id, "elapsed": elapsed})
return
time.sleep(self.poll_interval)
self.emit("pending", {"task_id": task_id, "elapsed": elapsed})
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": captcha_id,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
self.emit("solved", {
"task_id": task_id,
"solution": data["request"],
"duration": time.time() - start_time
})
return
elif data.get("request") != "CAPCHA_NOT_READY":
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": time.time() - start_time
})
return
# Usage
bus = CaptchaBus(os.environ["CAPTCHAAI_API_KEY"])
bus.on("submitted", lambda e: print(f"[SUBMIT] {e['task_id']}"))
bus.on("solved", lambda e: print(f"[SOLVED] {e['task_id']} in {e['duration']:.1f}s"))
bus.on("failed", lambda e: print(f"[FAILED] {e['task_id']} — {e['error']}"))
bus.on("timeout", lambda e: print(f"[TIMEOUT] {e['task_id']}"))
bus.submit("6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "https://example.com")
Fehlgeschlagene Aufgaben automatisch wiederholen
Weil die Wiederholungslogik selbst nur ein Listener auf failed ist, bleibt sie sauber vom Lösungscode getrennt. Ein Zähler begrenzt die Versuche, danach gibt der Handler auf – so vermeiden Sie Endlosschleifen bei dauerhaft fehlerhaften Parametern.
// Automatic retry on failure
bus.on("failed", async (e) => {
if (e.retryCount >= 3) {
console.error(`[GIVE UP] ${e.taskId} after 3 retries`);
return;
}
console.log(`[RETRY] ${e.taskId} — attempt ${(e.retryCount || 0) + 1}`);
await bus.submit({
...e.originalParams,
_retryCount: (e.retryCount || 0) + 1,
});
});
Promise-Wrapper für async/await
Manchmal möchten Sie eine einzelne Lösung einfach awaiten, statt Listener zu verdrahten. Legen Sie dafür eine Promise-basierte API über den Event Bus – der Wrapper räumt seine Listener nach solved, failed oder timeout selbst wieder auf.
function solveCaptcha(bus, params) {
return new Promise((resolve, reject) => {
const taskId = bus.submit(params);
function onSolved(e) {
if (e.taskId === taskId) {
cleanup();
resolve(e.solution);
}
}
function onFailed(e) {
if (e.taskId === taskId) {
cleanup();
reject(new Error(e.error));
}
}
function cleanup() {
bus.removeListener("solved", onSolved);
bus.removeListener("failed", onFailed);
bus.removeListener("timeout", onFailed);
}
bus.on("solved", onSolved);
bus.on("failed", onFailed);
bus.on("timeout", onFailed);
});
}
// Usage
const solution = await solveCaptcha(bus, {
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
Der Event Bus im Produktivbetrieb
Im Dauerbetrieb läuft ein solcher Bus typischerweise in einem Worker-Prozess auf einem VPS – bei DACH-Teams oft auf Hetzner, IONOS oder netcup. Da CaptchaAI pro Thread abrechnet und nicht pro Lösung, bestimmt Ihre gebuchte Thread-Zahl, wie viele CAPTCHAs gleichzeitig durch den Bus laufen können; schon der Einstiegstarif BASIC (15 $/Monat, 5 Threads) erlaubt fünf parallele Vorgänge, größere Tarife entsprechend mehr. Das solved-Ereignis eignet sich gut, um die Durchlaufzeit an ein GitLab-CI-Dashboard oder Prometheus zu melden.
Wenn Ihre Automatisierung dabei Daten von Drittseiten einsammelt, beachten Sie den DSGVO-Kontext: IP-Adressen gelten als personenbezogene Daten. Prüfen Sie Ihre Datenflüsse und die Rechtsgrundlage, bevor Sie Scraping-Workloads in Produktion nehmen – das ist Sorgfaltspflicht auf Ihrer Seite, keine Aussage über CaptchaAI.
Fehlerbehebung
| Problem | Ursache | Lösung |
|---|---|---|
| Ein Listener reagiert nicht | Ereignisname vertippt (z. B. „solve" statt „solved") | Gleichen Sie die Namen in emit und on exakt ab |
| Warnung wegen Speicherleck | Zu viele Listener auf einem Ereignis | Mit setMaxListeners() anheben oder Listener nach Gebrauch entfernen |
| Token wird erzeugt, aber vom Ziel abgelehnt | sitekey, pageurl oder Session-Kontext passen nicht zusammen | Parameter erneut erfassen und das Token in derselben Browser- oder HTTP-Sitzung einsetzen |
| Polling läuft ständig ins Timeout | Intervall oder Wartezeit zu eng gesetzt | Alle 5–10 Sekunden abfragen und Timeout von echten Fehlercodes trennen |
| Beispiel läuft lokal, im Workflow aber nicht | Token wird nicht ins richtige Formularfeld der Zielkette eingetragen | Übergabepfad vom Solver bis zur finalen Zielanfrage prüfen |
Häufige Fragen
Wie viele CAPTCHAs kann ich parallel über den Bus lösen?
So viele, wie Ihr Tarif an Threads erlaubt. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – ein Thread verarbeitet ein CAPTCHA und ist danach sofort wieder frei. BASIC (15 $/Monat) umfasst 5 Threads, ADVANCE (90 $/Monat) 50 Threads. Der Bus selbst begrenzt nichts; die Obergrenze ist Ihre Thread-Zuteilung.
Was passiert, wenn ein Token nach dem Lösen abläuft?
Setzen Sie das Token unmittelbar nach dem solved-Ereignis ein. Lösungstoken sind kurzlebig – bei reCAPTCHA rund 120 Sekunden – und werden vom Ziel abgelehnt, sobald sie verfallen sind. Verarbeiten Sie die Lösung deshalb direkt in der Zielanfrage, statt sie zwischenzuspeichern.
Wann sollte ich auf einen externen Message-Broker umsteigen?
Bei einem einzelnen Prozess ist der eingebaute EventEmitter einfacher und schneller. Sobald mehrere Prozesse oder Services auf dieselben CAPTCHA-Ereignisse reagieren müssen, greifen Sie zu Redis, RabbitMQ oder Kafka. Für die meisten In-Process-Pipelines bleibt der leichtgewichtige Bus die passende Wahl.
Unterstützt der Bus auch reCAPTCHA v3, Turnstile und GeeTest v3?
Ja. Die Architektur ist typunabhängig – Sie ändern nur den method-Parameter (userrecaptcha, turnstile, geetest) und die zugehörigen Felder. Der Lebenszyklus aus übermittelt, ausstehend, gelöst und fehlgeschlagen bleibt für alle unterstützten CAPTCHA-Typen identisch.
Verwandte Leitfäden
- CaptchaAI in wenigen Minuten einrichten
- API-Antwortformate und Fehlercodes verstehen
- reCAPTCHA v2 per API lösen