DevOps y Escalado

Creación de resolución de CAPTCHA basada en eventos con AWS SNS y CaptchaAI

¿Quieres que tu scraper deje de esperar a que cada CAPTCHA termine de resolverse? La respuesta es invertir el flujo: en lugar de consultar el resultado en bucle, deja que CaptchaAI te avise cuando esté listo. Con AWS SNS (Simple Notification Service) montas justo eso —una arquitectura basada en eventos donde CaptchaAI entrega cada solución a un callback, ese callback la publica en un topic de SNS y varios consumidores (una cola SQS, una Lambda de auditoría, una alerta por correo) la procesan por su cuenta.

Para una agencia que monitoriza precios en marketplaces como MercadoLibre o Amazon.es a gran escala, esto tiene una consecuencia concreta: el equipo que mantiene el scraping y el que procesa los resultados evolucionan por separado, sin pisarse. En este tutorial construimos el pipeline completo, paso a paso, en Python y JavaScript.

Cómo funciona la arquitectura basada en eventos

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

La idea central es una sola: SNS hace fan-out (distribución). Un mismo resultado de CAPTCHA dispara varios consumidores sin que el callback sepa quiénes son ni cuántos hay. Añadir mañana un panel de analítica o una segunda cola es cuestión de crear una suscripción más —el scraper ni se entera.

Este patrón conviene cuando ya resuelves volumen (miles de CAPTCHA por hora) o cuando distintos equipos consumen el mismo resultado. Para un script puntual que resuelve un puñado de desafíos, el sondeo (polling) directo sigue siendo más simple; el desacoplamiento se paga en piezas de infraestructura, así que úsalo cuando esa flexibilidad rinde.

Paso 1: crear el topic de SNS

Empieza creando el topic al que se suscribirá todo lo demás. Puedes hacerlo desde la CLI de AWS o con boto3 en Python; en ambos casos guarda el ARN que devuelve, porque es el identificador que reutilizarás en el resto de los pasos.

aws sns create-topic --name captcha-results --output text
# Returns: arn:aws:sns:us-east-1:123456789:captcha-results
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}")

Paso 2: construir el receptor del callback

El callback es una función Lambda mínima detrás de API Gateway. Su único trabajo es tomar el resultado que envía CaptchaAI y publicarlo en SNS: nada de lógica de negocio aquí, para que arranque rápido y sea fácil de razonar.

Python (handler de 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 (handler de 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" };
};

Paso 3: enviar el CAPTCHA con la URL de callback

Ahora indícale a CaptchaAI a dónde entregar el resultado. El parámetro pingback apunta a la URL de tu endpoint de API Gateway; a partir de ahí, cada tarea que envíes dispara un callback en cuanto se resuelve. Este envío usa el flujo estándar de reCAPTCHA como ejemplo, pero el pingback funciona igual para cualquier tipo compatible.

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

Paso 4: suscribir a los consumidores

Con el topic en pie, suscribe los consumidores que necesites. Cada suscripción recibe su propia copia del mismo mensaje, así que puedes mezclar destinos sin que compitan por el resultado. Los tres más habituales son una cola SQS que almacena todos los resultados, una Lambda que registra una traza de auditoría y una suscripción por correo que avisa cuando algo falla:

# 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
)
# 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
)
# Subscribe email for error notifications with filter
sns.subscribe(
    TopicArn=topic_arn,
    Protocol="email",
    Endpoint="ops@example.com"
)

Paso 5: consumir los resultados desde SQS

El último cambio está en el scraper: en vez de sondear a CaptchaAI, lee las soluciones desde SQS. Esto es lo que saca el polling del camino crítico y libera los hilos que antes quedaban a la espera.

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

Filtrado de mensajes en SNS

SNS puede enrutar según los atributos del mensaje: con una FilterPolicy consigues, por ejemplo, que solo los fallos lleguen a la cola de operaciones y el resto siga su camino normal.

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

Solución de problemas frecuentes

La mayoría de los fallos al montar este patrón caen en cuatro categorías: el callback rechazado, los mensajes que no llegan, los duplicados y la latencia del arranque en frío. Esta tabla resume la causa y el arreglo de cada uno:

Problema Causa Solución
El callback devuelve 403 La autenticación de API Gateway bloquea a CaptchaAI Deshabilita la autenticación en la ruta del callback y valida con un token en su lugar
Los mensajes no llegan a SQS Falta el permiso SNS → SQS Añade el permiso sns:Publish a la política de la cola SQS
Se procesan resultados duplicados SNS entrega al menos una vez Aplica idempotencia: verifica el task_id antes de procesar
El arranque en frío de Lambda retrasa el callback No configuraste la concurrencia aprovisionada Habilita la concurrencia aprovisionada en la Lambda del callback

Preguntas frecuentes

¿Necesito API Gateway o puedo llamar a la Lambda directamente?

Necesitas una URL HTTP pública para el pingback, y API Gateway es la forma estándar de exponer una Lambda. También sirve una Function URL de Lambda: lo que CaptchaAI requiere es un endpoint HTTPS al que entregar el resultado.

¿Cómo evito procesar dos veces el mismo resultado?

SNS entrega cada mensaje al menos una vez, así que los duplicados son normales. Guarda el task_id ya procesado (en Redis o DynamoDB) y descarta cualquier repetido antes de actuar; con esa comprobación el pipeline queda idempotente.

¿Qué pasa si un consumidor falla al procesar un mensaje?

Configura una cola de mensajes fallidos (dead-letter queue) en la suscripción SQS. Tras los reintentos, el mensaje que no se procesa acaba ahí en lugar de perderse, y puedes revisarlo o reprocesarlo con calma.

¿Este patrón funciona con cualquier tipo de CAPTCHA?

Sí. Es agnóstico al tipo: la arquitectura es idéntica para reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3 o CAPTCHA de imagen, porque SNS solo transporta el resultado; quien lo resuelve es CaptchaAI antes de disparar el callback.

Artículos relacionados

Empieza hoy: obtén tu API key de CaptchaAI, apunta el pingback a tu API Gateway y deja que SNS reparta cada resultado.

Guías relacionadas:

Los comentarios están deshabilitados para este artículo.