DevOps & Mise à l'Échelle

Construire une résolution CAPTCHA orientée événements avec AWS SNS et CaptchaAI

Le polling direct des résultats CAPTCHA garde inutilement des threads occupés et couple votre scraper au pipeline de résolution. Une architecture event-driven corrige ce problème. CaptchaAI envoie le résultat à un callback, ce callback publie un événement dans SNS, puis plusieurs consommateurs récupèrent l'information selon leur rôle : stockage, audit, alerting ou reprise du workflow.

Ce modèle devient intéressant dès que plusieurs composants doivent réagir au même résultat sans se connaître entre eux.

Vue d'ensemble de l'architecture

[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 joue ici le rôle de fan-out : un seul résultat peut alimenter plusieurs traitements en aval sans alourdir le callback.

Étape 1 : créer le topic SNS

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}")

Étape 2 : construire le récepteur de callback

Cette fonction Lambda reçoit la réponse de CaptchaAI puis la republie dans SNS afin de découpler totalement le point d'entrée HTTP des traitements aval.

Python (gestionnaire Lambda)

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 (gestionnaire Lambda)

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" };
};

Étape 3 : soumettre les CAPTCHA avec une URL de callback

Il faut ensuite déclarer l'URL de callback dans le paramètre pingback au moment de la soumission.

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')}")

Étape 4 : abonner les consommateurs

File SQS pour stocker les résultats

# 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 pour l'audit

# 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
)

Email pour les alertes

# Subscribe email for error notifications with filter
sns.subscribe(
    TopicArn=topic_arn,
    Protocol="email",
    Endpoint="ops@example.com"
)

Étape 5 : consommer les résultats depuis SQS

Votre scraper ou votre orchestrateur peut alors lire la réponse depuis SQS au lieu d'interroger CaptchaAI en boucle.

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;
}

Filtrage des messages SNS

Vous pouvez aussi router différemment les messages selon leur statut ou leur type.

# 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"]
        })
    }
)

Depannage

Probleme Cause probable Correctif
Le callback renvoie 403 La route API Gateway bloque CaptchaAI Ouvrez cette route ou ajoutez une validation compatible
Les messages n'arrivent pas dans SQS Permissions SNS vers SQS manquantes Ajoutez la policy sns:Publish côté file
Des doublons sont traités Livraison at-least-once de SNS Rendez le traitement idempotent via task_id
Le callback est lent au premier appel Cold start Lambda Activez la provisioned concurrency si nécessaire

FAQ

Pourquoi passer par SNS au lieu de tout traiter dans le callback ?

Parce que cela garde le callback simple et rapide. Vous pouvez ensuite ajouter du logging, du stockage, de l'alerting ou des traitements métier sans modifier le point d'entrée.

Quelle surcharge ajoute SNS ?

Très peu au regard du temps de résolution d'un CAPTCHA. Dans la plupart des architectures, ce surcoût est marginal face au gain de découplage.

Peut-on imposer un ordre de traitement ?

Oui, avec SNS FIFO et SQS FIFO si votre pipeline l'exige. Il faut alors concevoir explicitement la clé de regroupement et la stratégie de consommation.

Articles connexes

Prochaines étapes

Construisez votre pipeline event-driven, puis récupérez votre clé API CaptchaAI pour la connecter à votre stack AWS.

Guides associés :

Les commentaires sont désactivés pour cet article.