Eine Dead-Letter-Queue (DLQ) ist der Auffangbehälter für CAPTCHA-Aufgaben, die auch nach allen Wiederholungsversuchen nicht gelöst wurden. Statt in einer Log-Zeile zu verschwinden, landen sie an einem definierten Ort – bereit für einen späteren erneuten Versuch, eine Auswertung oder eine Warnmeldung. Dieser Leitfaden zeigt Schritt für Schritt, wie Sie eine DLQ für CaptchaAI-Tasks in Python und Node.js aufbauen und kontrolliert wieder abarbeiten.
Warum CAPTCHA-Aufgaben fehlschlagen
Typische Gründe, warum eine CAPTCHA-Aufgabe in der DLQ landet:
ERROR_CAPTCHA_UNSOLVABLE– der Solver konnte die CAPTCHA-Abfrage nicht lösenERROR_NO_SLOT_AVAILABLE– alle Worker ausgelastet, Wiederholungsversuche aufgebraucht- Timeout – der Solver hat innerhalb der Frist kein Ergebnis geliefert
- Netzwerkfehler – die Verbindung brach während des Pollings ab
Für den Umgang mit diesen Fehlern hilft eine einfache Unterscheidung: Vorübergehende Fehler (ERROR_NO_SLOT_AVAILABLE, Timeouts, Netzwerkabbrüche) klingen von selbst wieder ab und rechtfertigen einen erneuten Versuch. Dauerhafte Fehler – ein falscher Sitekey, eine veraltete Page-URL, ein leeres Guthaben – wiederholen sich bei jedem Anlauf und gehören protokolliert statt endlos neu eingereiht. Die DLQ bildet genau diese zweite Chance strukturiert ab, ohne die erste Gruppe mit der zweiten zu verwechseln.
In der Praxis treten solche Fehler gehäuft in automatisierten Pipelines auf – etwa in einem nächtlichen Scraping-Job, der als GitLab-CI-Pipeline auf einem Hetzner-Server läuft und dem niemand live zusieht. Ohne DLQ erzeugt jeder Fehlschlag nur eine Log-Zeile und ist am nächsten Morgen vergessen; mit DLQ liegt die Aufgabe reproduzierbar vor, samt Fehlermeldung und Versuchszähler.
In-Memory oder persistent: die richtige DLQ wählen
Bevor Sie Code schreiben, lohnt eine Grundsatzentscheidung: Soll die Warteschlange nur im Arbeitsspeicher leben oder einen Neustart überstehen?
- In-Memory eignet sich für kurzlebige Skripte und einmalige Batch-Läufe. Sie ist in wenigen Zeilen umgesetzt und braucht keine Infrastruktur – verliert ihren Inhalt aber, sobald der Prozess endet.
- Persistent (Datei, Redis oder eine echte Message-Queue wie RabbitMQ) ist Pflicht für langlaufende Dienste und verteilte Worker. Hier darf ein Deployment, ein Absturz oder ein Skalierungsereignis keine offenen Aufgaben verschlucken.
Als Faustregel: Läuft Ihr Prozess länger als ein paar Minuten oder verteilt sich auf mehrere Worker, wählen Sie eine persistente DLQ. Die folgenden Beispiele zeigen beide Wege – erst die In-Memory-Variante in Python, dann eine dateigestützte in Node.js.
Hinweis: Eine DLQ ersetzt keine Wiederholungslogik, sondern ergänzt sie. Erst wenn die regulären Retries erschöpft sind, sollte eine Aufgabe überhaupt in der DLQ landen – sonst vermischen Sie normale Übergangsfehler mit echten Dauerfehlern.
Python: eine In-Memory-DLQ Schritt für Schritt aufbauen
Die einfachste Variante hält die fehlgeschlagenen Aufgaben in einer deque im Arbeitsspeicher. Nach erschöpften Retries wandert die Aufgabe samt Fehlermeldung und Versuchszähler in die DLQ:
import time
import json
import requests
from collections import deque
from dataclasses import dataclass, asdict
from typing import Optional
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
@dataclass
class FailedTask:
sitekey: str
page_url: str
error: str
attempts: int
timestamp: float
task_id: Optional[str] = None
class DeadLetterQueue:
def __init__(self, max_size=1000, max_retries=3):
self._queue = deque(maxlen=max_size)
self.max_retries = max_retries
def push(self, task: FailedTask):
self._queue.append(task)
print(f"[dlq] Added: {task.error} (attempts: {task.attempts})")
def pop(self) -> Optional[FailedTask]:
return self._queue.popleft() if self._queue else None
def size(self) -> int:
return len(self._queue)
def peek_all(self) -> list:
return [asdict(t) for t in self._queue]
def export_json(self, path: str):
with open(path, "w") as f:
json.dump(self.peek_all(), f, indent=2)
print(f"[dlq] Exported {self.size()} tasks to {path}")
dlq = DeadLetterQueue(max_retries=3)
def solve_captcha(sitekey, page_url, max_retries=3):
for attempt in range(max_retries + 1):
try:
resp = requests.post(SUBMIT_URL, data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}, timeout=15)
data = resp.json()
if data["status"] != 1:
raise Exception(data["request"])
task_id = data["request"]
for _ in range(24):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": "1",
}, timeout=15).json()
if poll["status"] == 1:
return poll["request"]
if poll["request"] != "CAPCHA_NOT_READY":
raise Exception(poll["request"])
raise TimeoutError(f"Task {task_id} timed out")
except Exception as e:
if attempt == max_retries:
dlq.push(FailedTask(
sitekey=sitekey,
page_url=page_url,
error=str(e),
attempts=attempt + 1,
timestamp=time.time(),
))
return None
time.sleep(2 ** attempt)
return None
# Process a batch
urls = [f"https://example.com/page/{i}" for i in range(5)]
for url in urls:
token = solve_captcha("6Le-SITEKEY", url)
if token:
print(f"Solved: {token[:40]}...")
print(f"\nDLQ size: {dlq.size()}")
Erwartete Ausgabe:
Solved: 03AGdBq26ZfPxL...
Solved: 03AGdBq27AbCdE...
[dlq] Added: ERROR_CAPTCHA_UNSOLVABLE (attempts: 4)
Solved: 03AGdBq28FgHiJ...
[dlq] Added: Task 71823460 timed out (attempts: 4)
DLQ size: 2
Zwei von fünf Aufgaben sind gescheitert und liegen jetzt sicher in der DLQ – bereit für einen zweiten Anlauf, statt einfach verworfen zu werden.
Aufgaben aus der DLQ erneut verarbeiten
Nach dem Hauptlauf arbeiten Sie die DLQ gezielt ab. Entscheidend ist eine harte Obergrenze für die Gesamtzahl der Versuche, damit dieselbe Aufgabe nicht endlos kreist:
def retry_dlq(dlq: DeadLetterQueue, max_retries=2):
retried = 0
recovered = 0
while dlq.size() > 0:
task = dlq.pop()
if task.attempts >= dlq.max_retries + max_retries:
print(f"[dlq] Permanently failed: {task.sitekey} — {task.error}")
continue
retried += 1
token = solve_captcha(
task.sitekey, task.page_url, max_retries=max_retries
)
if token:
recovered += 1
print(f"[dlq-retry] Recovered: {token[:40]}...")
print(f"[dlq] Retried: {retried}, Recovered: {recovered}")
# Run DLQ retry after main batch
retry_dlq(dlq)
Node.js: eine persistente DLQ mit Dateispeicher
Eine reine In-Memory-DLQ verliert alle Aufgaben, sobald der Prozess neu startet. Für langlaufende Dienste schreiben Sie die Warteschlange deshalb in eine Datei – hier in Node.js:
const fs = require('fs');
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const DLQ_FILE = './captcha-dlq.json';
class DeadLetterQueue {
constructor(maxRetries = 3) {
this.maxRetries = maxRetries;
this.queue = this._load();
}
push(task) {
this.queue.push({
...task,
timestamp: Date.now(),
});
this._save();
console.log(`[dlq] Added: ${task.error} (attempts: ${task.attempts})`);
}
pop() {
const task = this.queue.shift();
if (task) this._save();
return task || null;
}
size() {
return this.queue.length;
}
_load() {
try {
return JSON.parse(fs.readFileSync(DLQ_FILE, 'utf8'));
} catch {
return [];
}
}
_save() {
fs.writeFileSync(DLQ_FILE, JSON.stringify(this.queue, null, 2));
}
}
const dlq = new DeadLetterQueue(3);
async function solveCaptcha(sitekey, pageurl, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: { key: API_KEY, method: 'userrecaptcha', googlekey: sitekey, pageurl, json: 1 }
});
if (submit.data.status !== 1) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 }
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
}
throw new Error(`Task ${taskId} timed out`);
} catch (err) {
if (attempt === maxRetries) {
dlq.push({ sitekey, pageurl, error: err.message, attempts: attempt + 1 });
return null;
}
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
}
// Process tasks
(async () => {
for (let i = 0; i < 5; i++) {
const token = await solveCaptcha('6Le-SITEKEY', `https://example.com/page/${i}`);
if (token) console.log(`Solved: ${token.substring(0, 40)}...`);
}
console.log(`DLQ size: ${dlq.size()}`);
})();
Weil jeder push und pop die Datei aktualisiert, übersteht die DLQ so einen Neustart oder Absturz des Workers.
Fehlermuster in der DLQ analysieren
Der eigentliche Wert einer DLQ liegt nicht nur in der Wiederherstellung, sondern in den Mustern, die sie sichtbar macht. Exportieren Sie die gescheiterten Aufgaben und zählen Sie die Fehlerarten aus:
# Export DLQ for analysis
dlq.export_json("failed-tasks.json")
# Analyze error distribution
from collections import Counter
errors = Counter(t["error"] for t in dlq.peek_all())
for error, count in errors.most_common():
print(f" {error}: {count}")
Diese Auswertung hilft Ihnen dabei,
- Sitekeys zu identifizieren, die dauerhaft fehlschlagen – prüfen Sie, ob die Parameter stimmen
- Timeouts zu bestimmten Uhrzeiten zu erkennen – und sie mit der API-Auslastung zu korrelieren
- Netzwerkfehler aufzuspüren – und den Zustand Ihrer Proxys zu prüfen
Häuft sich in dieser Verteilung ein einzelner Fehlercode, ist das selten Zufall: Dominiert ERROR_CAPTCHA_UNSOLVABLE, stimmt oft der Sitekey oder die Page-URL nicht; dominieren Timeouts, ist meist die Zahl paralleler Threads zu knapp für das Aufkommen.
Die DLQ überwachen und rechtzeitig warnen
Eine DLQ ist nur so nützlich wie die Aufmerksamkeit, die ihr Füllstand bekommt. Behandeln Sie dlq.size() deshalb als Metrik, nicht als Debug-Ausgabe: Ab einem Schwellenwert – etwa 10 % der Batch-Größe – sollte eine Warnung ausgelöst werden.
- Exportieren Sie den Zähler an Ihr Monitoring (Prometheus, ein Grafana-Dashboard oder ein einfacher Webhook in einen Chat-Kanal).
- Ein plötzlich steigender Füllstand ist ein Frühindikator für ein systematisches Problem – ausgelastete Worker, falsche Parameter oder instabile Proxys – lange bevor es in den Ergebnissen auffällt.
- Kombinieren Sie die Warnung mit dem geplanten Drain: Wenn die DLQ nach einem
retry_dlq()-Lauf weiterhin voll ist, liegt ein dauerhafter Fehler vor, kein vorübergehender.
Ein belastbarer Überwachungsablauf lässt sich in drei Schritten aufsetzen:
dlq.size()bei jedem Durchlauf als Metrik veröffentlichen.- Einen Schwellenwert definieren und darüber alarmieren.
- Nach jedem geplanten Drain prüfen, ob der Füllstand tatsächlich sinkt.
Typische Probleme und Lösungen
| Problem | Ursache | Lösung |
|---|---|---|
| Die DLQ wächst unbegrenzt | Wiederholungen werden nie abgearbeitet | Regelmäßigen DLQ-Drain mit retry_dlq() einplanen |
| Dieselbe Aufgabe läuft endlos in der Schleife | Keine Obergrenze für die Versuche | task.attempts prüfen, bevor Sie erneut einreihen |
| DLQ-Datei ist beschädigt | Gleichzeitige Schreibzugriffe | Dateisperre nutzen oder auf Redis/Datenbank wechseln |
| Aufgaben gehen bei einem Absturz verloren | Reine In-Memory-DLQ | Datei- oder Redis-gestützte DLQ verwenden |
Häufige Fragen
Welche Fehler gehören in eine DLQ – und welche nicht?
In die DLQ gehören nur Aufgaben, die nach allen regulären Wiederholungen weiterhin scheitern. Vorübergehende Fehler wie ERROR_NO_SLOT_AVAILABLE, Timeouts oder Netzwerkabbrüche lassen sich meist per erneutem Versuch beheben und sollten erst nach erschöpften Retries in der DLQ landen. Dauerhafte Konfigurationsfehler – ein falscher Sitekey oder eine ungültige Page-URL – gehören protokolliert, nicht endlos wiederholt.
Wie oft sollte ich die DLQ automatisch abarbeiten?
Ein Drain-Intervall von wenigen Minuten bis Stunden passt für die meisten Pipelines. Planen Sie retry_dlq() als eigenen Schritt ein – etwa als Cron-Job oder als nachgelagerte Stufe Ihrer GitLab-CI-Pipeline –, damit vorübergehende Ausfälle abklingen können, bevor Sie es erneut versuchen. Eine Obergrenze für die Gesamtversuche bleibt Pflicht, sonst kreist dieselbe Aufgabe unbegrenzt.
Wie werde ich benachrichtigt, wenn die DLQ zu voll wird?
Überwachen Sie dlq.size() und lösen Sie ab einem Schwellenwert eine Warnung aus. Ein stark wachsender Zähler ist ein Frühindikator für ein systematisches Problem – falsche Parameter, ausgelastete Worker oder instabile Proxys. Hängen Sie den Zähler an Ihr Monitoring (etwa Prometheus oder einen einfachen Webhook), statt nur gelegentlich die Export-Datei manuell zu prüfen.
Lässt sich eine DLQ mit dem Circuit-Breaker-Muster kombinieren?
Ja. Der Circuit Breaker stoppt Anfragen während eines Ausfalls, die DLQ fängt alle Aufgaben auf, die scheitern, bevor der Schalter auslöst. Beide ergänzen sich: Der Breaker schützt vor Lastspitzen gegen einen ausgefallenen Dienst, die DLQ sichert die betroffenen Aufgaben. Mehr dazu im Circuit-Breaker-Muster für CAPTCHA-API-Aufrufe.
Kein fehlgeschlagener CAPTCHA-Task geht mehr verloren
Fangen Sie jede gescheiterte Aufgabe in einer Dead-Letter-Queue auf und arbeiten Sie sie kontrolliert erneut ab. Holen Sie sich Ihren API-Schlüssel auf captchaai.com.
Verwandte Leitfäden
- Circuit-Breaker-Muster für CAPTCHA-API-Aufrufe
- Wiederholungslogik für die CaptchaAI-API in Node.js
- Redis-Warteschlange + CaptchaAI: verteilte Verarbeitung