DevOps & Skalierung

Erstellen einer ereignisgesteuerten CAPTCHA-Lösung mit AWS SNS und CaptchaAI

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

Kommentare sind für diesen Artikel deaktiviert.