Ereignisgesteuert statt pollen: CaptchaAI schickt das fertige Ergebnis per Callback an ein API Gateway mit Lambda, das es in ein SNS-Topic veröffentlicht. AWS SNS (Simple Notification Service) verteilt die Lösung per Fan-Out an SQS und weitere Consumer – Ihr Scraper pollt nicht mehr, sondern liest fertige Lösungen aus der Queue.
Der Vorteil: Threads werden frei, sobald ein CAPTCHA gelöst ist, und neue Consumer kommen ohne Änderung am Callback-Handler hinzu. Da CaptchaAI pro Thread abrechnet und nicht pro Lösung, zahlt sich der entkoppelte Aufbau bei hohem Durchsatz aus.
Architektur im Überblick
[Scraper] → Submit CAPTCHA → [CaptchaAI API]
↓
Solve completes
↓
Callback → [API Gateway + Lambda]
↓
Publish → [SNS Topic]
↓
┌───────────────┼───────────────┐
↓ ↓ ↓
[SQS Queue] [Lambda Logger] [Email Alert]
(result store) (audit trail) (on failure)
SNS-Fan-Out heißt: Ein CAPTCHA-Ergebnis stößt mehrere Consumer an, ohne dass der Callback-Handler von ihnen weiß. Die Beispiele nutzen us-east-1; für EU-Deployments setzen Sie eu-central-1 (Frankfurt) – relevant bei personenbezogenen Ergebnisdaten und DSGVO-konformen Datenflüssen.
Warum ereignisgesteuert statt Polling?
Beim klassischen Polling fragt Ihr Scraper das Ergebnis in einer Schleife über res.php ab und hält dabei einen Thread samt offener Verbindung, bis die Lösung vorliegt. Das ereignisgesteuerte Muster dreht den Ablauf um: Sie reichen die Aufgabe ein, geben den Thread sofort frei und lassen sich das Ergebnis zustellen. Bei hohem Durchsatz bringt das drei konkrete Vorteile:
- Threads werden früher frei – ein Thread bleibt nur belegt, solange CaptchaAI aktiv löst, nicht während Ihrer Warteschleife.
- Lose Kopplung – neue Consumer für Logging, Alarmierung oder Analytics kommen hinzu, ohne dass Sie den Callback-Handler anfassen.
- Weniger Leerlauf-Anfragen – die wiederholten
res.php-Abfragen entfallen, die beim Polling Verbindungen binden.
Der Aufbau lohnt sich damit besonders dort, wo viele CAPTCHAs parallel laufen und jede eingesparte Thread-Sekunde direkt in die Abrechnung eingeht.
Schritt 1: SNS-Topic anlegen
Das SNS-Topic ist der zentrale Verteilpunkt: Alle gelösten CAPTCHAs laufen hier ein und werden von dort an die Abonnenten gefächert. Legen Sie es einmalig an – per CLI oder direkt aus dem Code.
AWS CLI
aws sns create-topic --name captcha-results --output text
# Returns: arn:aws:sns:us-east-1:123456789:captcha-results
Python (boto3)
import boto3
sns = boto3.client("sns", region_name="us-east-1")
response = sns.create_topic(Name="captcha-results")
topic_arn = response["TopicArn"]
print(f"Topic ARN: {topic_arn}")
Notieren Sie die zurückgegebene topic_arn – Sie referenzieren sie in jedem Abonnement und in jedem Publish-Aufruf.
Schritt 2: Callback-Empfänger als Lambda bauen
Diese Lambda nimmt das CaptchaAI-Callback-Ergebnis entgegen und veröffentlicht es an SNS – die Logik liegt bei den Consumern.
Python (Lambda-Handler)
import json
import os
import boto3
sns = boto3.client("sns")
TOPIC_ARN = os.environ["SNS_TOPIC_ARN"]
def lambda_handler(event, context):
"""Receive CaptchaAI callback and publish to SNS."""
# Parse query parameters from API Gateway
params = event.get("queryStringParameters", {}) or {}
task_id = params.get("id", "")
solution = params.get("code", "")
if not task_id or not solution:
return {"statusCode": 400, "body": "Missing id or code"}
# Publish to SNS
message = {
"task_id": task_id,
"solution": solution,
"status": "solved"
}
sns.publish(
TopicArn=TOPIC_ARN,
Message=json.dumps(message),
Subject="captcha-solved",
MessageAttributes={
"task_id": {
"DataType": "String",
"StringValue": task_id
}
}
)
return {"statusCode": 200, "body": "OK"}
JavaScript (Lambda-Handler)
const { SNSClient, PublishCommand } = require("@aws-sdk/client-sns");
const sns = new SNSClient({ region: "us-east-1" });
const TOPIC_ARN = process.env.SNS_TOPIC_ARN;
exports.handler = async (event) => {
const params = event.queryStringParameters || {};
const taskId = params.id;
const solution = params.code;
if (!taskId || !solution) {
return { statusCode: 400, body: "Missing id or code" };
}
const message = {
task_id: taskId,
solution: solution,
status: "solved",
};
await sns.send(
new PublishCommand({
TopicArn: TOPIC_ARN,
Message: JSON.stringify(message),
Subject: "captcha-solved",
MessageAttributes: {
task_id: { DataType: "String", StringValue: taskId },
},
})
);
return { statusCode: 200, body: "OK" };
};
Beide Handler bleiben bewusst schlank: Sie prüfen nur id und code und veröffentlichen das Ergebnis an SNS. Die eigentliche Auswertung übernehmen die nachgelagerten Consumer.
Schritt 3: CAPTCHAs mit Callback-URL übermitteln
Richten Sie pingback auf Ihren API-Gateway-Endpunkt; CaptchaAI ruft ihn nach dem Lösen auf:
Python
import os
import requests
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
CALLBACK_URL = os.environ["CALLBACK_GATEWAY_URL"] # API Gateway URL
def submit_captcha(sitekey, pageurl):
"""Submit CAPTCHA with SNS-backed callback."""
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"pingback": CALLBACK_URL,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
return data["request"] # task_id
raise RuntimeError(f"Submit failed: {data.get('request')}")
Der pingback-Parameter ist der einzige Unterschied zum synchronen Ablauf. CaptchaAI ruft die URL nach dem Lösen auf, sodass ein Polling über res.php im Normalfall entfällt.
Schritt 4: Consumer abonnieren
Ein Topic, beliebig viele Abonnenten – SQS als Ergebnisspeicher, Lambda fürs Audit-Log, E-Mail für Alarme. Jeder Abonnent erhält dieselbe Nachricht und verarbeitet sie unabhängig; fällt ein Consumer aus, laufen die übrigen unverändert weiter:
SQS-Queue (Ergebnisspeicherung)
# Subscribe an SQS queue to receive all results
sqs_arn = "arn:aws:sqs:us-east-1:123456789:captcha-results-queue"
sns.subscribe(
TopicArn=topic_arn,
Protocol="sqs",
Endpoint=sqs_arn
)
Lambda (Audit-Logger)
# Subscribe a Lambda for audit logging
lambda_arn = "arn:aws:lambda:us-east-1:123456789:function:captcha-audit-logger"
sns.subscribe(
TopicArn=topic_arn,
Protocol="lambda",
Endpoint=lambda_arn
)
E-Mail (Fehlerbenachrichtigungen)
# Subscribe email for error notifications with filter
sns.subscribe(
TopicArn=topic_arn,
Protocol="email",
Endpoint="ops@example.com"
)
Beim E-Mail-Protokoll bestätigen Empfänger das Abonnement einmalig per Double-Opt-In-Link, bevor die ersten Benachrichtigungen ankommen.
Schritt 5: Ergebnisse aus SQS lesen
SQS puffert die Ergebnisse, bis Ihr Scraper bereit ist. Mit Long Polling warten Sie bis zu 20 Sekunden pro Abruf, ohne die Queue mit Leeranfragen zu belasten. Löschen Sie jede Nachricht nach erfolgreicher Verarbeitung – sonst wird sie nach Ablauf der Sichtbarkeitsfrist erneut zugestellt. Ihr Scraper liest die Lösungen also aus SQS, statt CaptchaAI abzufragen:
Python
import json
import boto3
sqs = boto3.client("sqs", region_name="us-east-1")
QUEUE_URL = os.environ["SQS_QUEUE_URL"]
def get_solved_captcha(timeout=30):
"""Wait for a CAPTCHA solution from the SQS queue."""
response = sqs.receive_message(
QueueUrl=QUEUE_URL,
MaxNumberOfMessages=1,
WaitTimeSeconds=min(timeout, 20) # Long polling (max 20s)
)
messages = response.get("Messages", [])
if not messages:
return None
msg = messages[0]
# SNS wraps the message — unwrap it
sns_envelope = json.loads(msg["Body"])
result = json.loads(sns_envelope["Message"])
# Delete message after processing
sqs.delete_message(
QueueUrl=QUEUE_URL,
ReceiptHandle=msg["ReceiptHandle"]
)
return result
JavaScript
const {
SQSClient,
ReceiveMessageCommand,
DeleteMessageCommand,
} = require("@aws-sdk/client-sqs");
const sqs = new SQSClient({ region: "us-east-1" });
const QUEUE_URL = process.env.SQS_QUEUE_URL;
async function getSolvedCaptcha(timeout = 30) {
const response = await sqs.send(
new ReceiveMessageCommand({
QueueUrl: QUEUE_URL,
MaxNumberOfMessages: 1,
WaitTimeSeconds: Math.min(timeout, 20),
})
);
const messages = response.Messages || [];
if (messages.length === 0) return null;
const msg = messages[0];
const snsEnvelope = JSON.parse(msg.Body);
const result = JSON.parse(snsEnvelope.Message);
await sqs.send(
new DeleteMessageCommand({
QueueUrl: QUEUE_URL,
ReceiptHandle: msg.ReceiptHandle,
})
);
return result;
}
Beide Sprachvarianten entpacken zuerst den SNS-Umschlag (Body) und daraus die eigentliche Nachricht (Message) – SNS verschachtelt die Payload doppelt.
Nachrichten gezielt routen mit SNS-Filterrichtlinien
Eine Filterrichtlinie leitet etwa nur fehlgeschlagene Lösungen an eine separate Ops-Queue. So trennen Sie den Fehlerpfad sauber vom Normalbetrieb, ohne einen zweiten Callback zu bauen; ausgewertet werden dabei die MessageAttributes, die Sie beim Publish setzen:
# Only send failures to the ops queue
sns.subscribe(
TopicArn=topic_arn,
Protocol="sqs",
Endpoint=failure_queue_arn,
Attributes={
"FilterPolicy": json.dumps({
"status": ["failed", "error"]
})
}
)
Ohne passende MessageAttributes greift die Filterrichtlinie nicht, und die Nachricht wird verworfen, statt zugestellt zu werden.
Fehlerbehebung
Die häufigsten Stolpersteine betreffen Berechtigungen zwischen SNS, SQS und Lambda sowie das korrekte Nachrichten-Handling im Consumer:
| Problem | Ursache | Lösung |
|---|---|---|
| SNS-Abonnement kommt nicht an | Fehlende SQS-Zugriffsrichtlinie | Resource Policy der SQS-Queue prüfen und SNS-Publish-Berechtigung hinzufügen |
| Callback-URL antwortet mit 5xx | Lambda-Fehler oder fehlende IAM-Rolle | Lambda-Logs und API-Gateway-Logs prüfen; SNS-Publish-Rechte der Lambda-Rolle sicherstellen |
| Nachrichten werden mehrfach empfangen | delete_message fehlt nach Verarbeitung | SQS-Nachricht nach erfolgreichem Verarbeiten mit delete_message löschen |
| SNS-Filterrichtlinie greift nicht | Falsches JSON-Format oder fehlende MessageAttributes | FilterPolicy-JSON-Syntax und MessageAttributes im Publish-Aufruf prüfen |
Häufige Fragen
Spare ich mit dem Callback-Muster CaptchaAI-Threads?
Ein Thread bleibt nur belegt, bis die Lösung fertig ist. Der Callback erspart zusätzlich die res.php-Abfragen, die beim Polling Verbindungen binden.
Was tun, wenn der Callback einmal nicht ankommt?
Halten Sie einen Fallback bereit: Bleibt die SNS-Nachricht aus, fragen Sie das Ergebnis für die task_id klassisch über res.php ab. So bleibt die Pipeline robust.
Funktioniert das Muster mit reCAPTCHA v3, Turnstile und Bild-CAPTCHAs?
Ja, die Pipeline ist unabhängig vom CAPTCHA-Typ – nur method und die Parameter ändern sich. Ob reCAPTCHA v2/v3, Turnstile, GeeTest v3 oder Bild-CAPTCHA: das Ergebnis kommt an denselben pingback.
In welcher AWS-Region sollte ich das Topic betreiben?
In der Region, in der auch Ihre übrigen Consumer laufen – das spart regionsübergreifende Latenz und Transferkosten. Verarbeiten Sie personenbezogene Ergebnisdaten, spricht für DACH-Workflows eu-central-1 (Frankfurt); prüfen Sie Ihre Datenflüsse und die passende Rechtsgrundlage nach DSGVO selbst.
Wie verhindere ich, dass ein Ergebnis doppelt verarbeitet wird?
SNS und SQS stellen mindestens einmal zu, gelegentlich also doppelt. Machen Sie den Consumer idempotent: Prüfen Sie die task_id gegen einen bereits verarbeiteten Datensatz, bevor Sie handeln, und löschen Sie die SQS-Nachricht erst nach erfolgreicher Verarbeitung.
Verwandte Leitfäden
- CaptchaAI in wenigen Minuten einrichten
- API-Antwortformate und Fehlercodes verstehen
- reCAPTCHA v2 per API lösen