DevOps & Scaling

Construindo soluções CAPTCHA orientadas a eventos com AWS SNS e CaptchaAI

Se o seu scraper trava esperando o resultado de um CAPTCHA, o problema não é a velocidade da CaptchaAI — é a arquitetura de polling. Manter uma thread presa consultando o resultado a cada poucos segundos limita o paralelismo e acopla o consumidor ao serviço de resolução. O AWS SNS inverte esse fluxo: a CaptchaAI resolve o desafio e dispara um callback; o callback publica no SNS; e o SNS distribui o evento para quantos consumidores forem necessários — fila SQS para armazenar a solução, Lambda para auditoria, e-mail para alertas de falha — sem que nenhum saiba da existência dos outros. Este guia monta o pipeline do zero: criar o tópico, publicar a partir do callback e consumir a solução via SQS em vez de ficar consultando a API em loop.

Do polling ao fan-out: como fica a arquitetura

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

O tópico SNS é o ponto central: recebe um evento e replica para todos os consumidores inscritos ao mesmo tempo — fan-out de verdade, não uma fila única sendo revezada. Quer adicionar um novo consumidor, como um dashboard de métricas ou um alerta no Slack? Basta inscrevê-lo no tópico; o Lambda de callback nunca precisa saber que ele existe.

Etapa 1: crie o tópico SNS

Comece criando o tópico que vai centralizar os resultados. Duas formas equivalentes — CLI para inspecionar rápido, boto3 para automatizar como parte do seu provisionamento:

CLI da AWS

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

Guarde o ARN retornado — ele é referenciado em toda inscrição de consumidor a seguir. Se seus workers atendem majoritariamente tráfego brasileiro, vale criar esse mesmo tópico em sa-east-1 (São Paulo): o SNS é regional, e manter tópico e consumidores perto da carga reduz a latência do fan-out. O us-east-1 acima é só referência de sintaxe.

Etapa 2: monte o Lambda que recebe o callback

Essa função é o único ponto que fala tanto com a CaptchaAI quanto com o SNS: recebe o resultado do callback e publica no tópico.

Python (manipulador 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"}

Note o MessageAttributes com o task_id — é ele que permite filtrar mensagens mais adiante. Se o Lambda de auditoria grava esse payload em um datastore persistente, trate isso como parte do inventário de dados sob a LGPD: basta reter task_id e status, sem dados do usuário final que originou a submissão.

JavaScript (manipulador 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" };
};

A versão em Node.js segue a mesma lógica com o SDK v3 da AWS; escolha conforme o runtime já usado no restante do seu callback.

Etapa 3: aponte o pingback para o API Gateway

Para que a CaptchaAI chame esse Lambda automaticamente, informe a URL do API Gateway no parâmetro pingback ao enviar a tarefa:

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

Assim que a CaptchaAI resolve o desafio, ela mesma chama esse endpoint — seu código de submissão não precisa mais consultar o resultado em loop.

Etapa 4: inscreva os consumidores

Com o tópico publicando eventos, inscreva quantos consumidores fizerem sentido para o seu pipeline. Nenhum interfere nos outros.

Fila SQS (armazenamento de resultados)

# 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 (registrador de auditoria)

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

Use esse logger para manter um histórico auditável sem sobrecarregar o Lambda de callback.

E-mail (alertas de falha)

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

Combine essa assinatura com o filtro da próxima seção para restringir os e-mails a resultados de falha.

Etapa 5: consuma o resultado a partir do SQS

O raspador para de fazer polling na API da CaptchaAI e passa a ler a fila SQS — o resultado já está lá quando o desafio termina de ser resolvido.

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

O SNS envolve a mensagem original em um envelope (Message como string JSON) — sempre desembrulhe antes de usar o resultado, como no json.loads duplo acima.

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

Só apague a mensagem depois de processá-la com sucesso; se o processamento falhar, deixe o SQS reentregar depois do tempo de visibilidade em vez de forçar um delete cedo demais.

Roteie por status: nem todo consumidor precisa ver tudo

Um filtro de assinatura evita, por exemplo, que a fila de alertas de operação receba resultados de sucesso junto com as falhas:

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

O filtro roda no lado do SNS, antes da entrega — o consumidor nunca chega a ver as mensagens que não passam no filtro, então você não paga o custo de processar e descartar.

Solução de problemas

Problema Causa Correção
Callback retorna 403 Autenticação do API Gateway bloqueia a chamada da CaptchaAI Desative a autenticação na rota do callback e valide por token no corpo, não no gateway
Mensagens não chegam na fila SQS Falta a permissão sns:Publish na política da fila Adicione essa permissão à política de acesso da fila SQS
Resultado processado em duplicidade O SNS entrega no modelo "pelo menos uma vez" Implemente idempotência: verifique o task_id antes de processar
Cold start do Lambda atrasa o callback Simultaneidade provisionada não configurada Habilite simultaneidade provisionada no Lambda de callback

Perguntas frequentes

Preciso pagar algo além do plano da CaptchaAI para usar essa arquitetura?

Sim, mas o custo é marginal. O SNS cobra por publicação e entrega, com tier gratuito generoso; no volume típico de CAPTCHA isso fica na casa de centavos por mês. O que determina o custo continua sendo o número de threads do seu plano CaptchaAI — por exemplo, BASIC (US$ 15/mês, 5 threads) — o SNS não muda esse cálculo.

O que acontece se o Lambda de callback falhar antes de publicar no SNS?

A CaptchaAI não reenvia o callback indefinidamente — existe uma janela curta de novas tentativas. Se o Lambda falhar de forma persistente nessa janela, o resultado se perde. Monitore erros do Lambda no CloudWatch separadamente da fila de falhas do SNS: são camadas de falha diferentes.

Dá para usar SNS FIFO se eu precisar manter a ordem em que os CAPTCHAs foram enviados?

Sim. Troque o tópico padrão por um SNS FIFO com fila SQS FIFO e defina MessageGroupId como o task_id para manter a ordem dentro do mesmo grupo. Sem isso, a ordem de entrega não é fixa.

Preciso migrar todo o pipeline de uma vez, ou dá para rodar SNS e polling em paralelo durante a transição?

Não precisa ser tudo de uma vez. Aponte o pingback só para os workers que já usam SNS e mantenha o polling nos demais enquanto valida o novo fluxo — os dois métodos não conflitam, já que o pingback é definido por submissão. Depois de confirmar que os consumidores recebem tudo, desative o polling remanescente.

Artigos relacionados

Próximas etapas

Monte seu próprio pipeline orientado a eventos — crie sua chave de API da CaptchaAI e conecte o callback ao tópico SNS ainda hoje.

Guias relacionados:

Os comentários estão desativados para este artigo.