DevOps y Escalado

AWS Lambda + CaptchaAI: resolución de CAPTCHA sin servidor

Puedes resolver un CAPTCHA sin mantener un solo servidor encendido: una función de AWS Lambda recibe la petición, llama a la API de CaptchaAI, devuelve el token y se apaga hasta la siguiente invocación. No pagas por tiempo de inactividad, la concurrencia crece sola cuando llegan más solicitudes y la lógica de resolución vive en un único archivo que puedes desplegar en minutos.

Ese modelo encaja especialmente bien con cargas irregulares. Una agencia en Madrid o en Ciudad de México que hace QA sobre formularios protegidos por CAPTCHA rara vez tiene un volumen constante: hay picos cuando se ejecuta la suite de pruebas y silencio el resto del día. Con Lambda pagas por invocación en USD, un costo predecible frente a un servidor encendido las 24 horas, y esta guía te muestra el handler, el disparo por SQS, la gestión segura de la clave y el despliegue con SAM.


El handler de Lambda que resuelve el CAPTCHA

El núcleo es una sola función. Recibe el evento (venga de API Gateway como cuerpo JSON o como invocación directa), extrae el method y los params, envía la tarea a in.php, sondea res.php hasta obtener el token y devuelve una respuesta HTTP limpia. Todo con urllib de la biblioteca estándar, sin dependencias externas que empaquetar.

# lambda_function.py
import json
import os
import time
import urllib.request
import urllib.parse


def lambda_handler(event, context):
    """AWS Lambda handler for CaptchaAI solving."""
    api_key = os.environ["CAPTCHAAI_KEY"]

    # Parse input
    body = json.loads(event.get("body", "{}")) if isinstance(event.get("body"), str) else event

    method = body.get("method", "userrecaptcha")
    params = body.get("params", {})

    try:
        token = solve_captcha(api_key, method, params)
        return {
            "statusCode": 200,
            "body": json.dumps({"token": token}),
        }
    except Exception as e:
        return {
            "statusCode": 500,
            "body": json.dumps({"error": str(e)}),
        }


def solve_captcha(api_key, method, params, timeout=90):
    """Solve CAPTCHA using CaptchaAI API."""
    # Submit task
    submit_data = urllib.parse.urlencode({
        "key": api_key,
        "method": method,
        "json": 1,
        **params,
    }).encode()

    req = urllib.request.Request(
        "https://ocr.captchaai.com/in.php",
        data=submit_data,
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        result = json.loads(resp.read())

    if result.get("status") != 1:
        raise RuntimeError(f"Submit error: {result.get('request')}")

    task_id = result["request"]

    # Poll for result
    start = time.time()
    while time.time() - start < timeout:
        time.sleep(5)
        poll_url = (
            f"https://ocr.captchaai.com/res.php"
            f"?key={api_key}&action=get&id={task_id}&json=1"
        )
        with urllib.request.urlopen(poll_url, timeout=15) as resp:
            data = json.loads(resp.read())

        if data["request"] != "CAPCHA_NOT_READY":
            if data.get("status") == 1:
                return data["request"]
            raise RuntimeError(f"Solve error: {data['request']}")

    raise TimeoutError("Solve timeout")

El patrón de dos pasos (enviar y sondear) es idéntico para reCAPTCHA v2/v3, Cloudflare Turnstile o GeeTest v3: solo cambian el method y los params. Cada solicitud en curso ocupa un thread de tu plan de CaptchaAI, así que la concurrencia de Lambda y tus threads contratados deben ir de la mano.


Protege tu API key con Secrets Manager

Nunca dejes la clave escrita en el código ni en un repositorio. Guárdala en AWS Secrets Manager y recupérala en tiempo de ejecución, o inyéctala como variable de entorno resuelta desde el secreto (lo verás en la plantilla SAM más abajo).

import json
import boto3


def get_api_key():
    """Retrieve CaptchaAI key from AWS Secrets Manager."""
    client = boto3.client("secretsmanager")
    response = client.get_secret_value(SecretId="captchaai/api-key")
    secret = json.loads(response["SecretString"])
    return secret["api_key"]

Crea el secreto una sola vez desde la terminal:

aws secretsmanager create-secret \
  --name captchaai/api-key \
  --secret-string '{"api_key":"YOUR_API_KEY"}'

Infraestructura como código con una plantilla SAM

Define la función, el endpoint HTTP y los permisos en un solo archivo template.yaml. Fíjate en cómo el valor de CAPTCHAAI_KEY se resuelve directamente desde Secrets Manager: la clave nunca aparece en texto plano y la política de IAM concede solo el permiso de lectura sobre ese secreto.

# template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 120
    MemorySize: 256
    Runtime: python3.11

Resources:
  CaptchaSolverFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: lambda_function.lambda_handler
      Environment:
        Variables:
          CAPTCHAAI_KEY: !Sub "{{resolve:secretsmanager:captchaai/api-key:SecretString:api_key}}"
      Events:
        SolveApi:
          Type: Api
          Properties:
            Path: /solve
            Method: post
      Policies:

        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:captchaai/api-key-*"

Outputs:
  SolveApiUrl:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/solve"

Despliegue y prueba de la función

Con la plantilla lista, sam build empaqueta el código y sam deploy --guided lo publica y te devuelve la URL del endpoint. Después, una llamada con curl confirma que todo el circuito responde de extremo a extremo.

# Build and deploy
sam build
sam deploy --guided

# Test
curl -X POST https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/Prod/solve \
  -H "Content-Type: application/json" \
  -d '{
    "method": "userrecaptcha",
    "params": {
      "googlekey": "SITE_KEY",
      "pageurl": "https://example.com"
    }
  }'

Procesamiento por lotes con SQS

Cuando no necesitas una respuesta inmediata, desacopla la resolución con una cola. Publica cada tarea en SQS y deja que Lambda las procese en lotes: absorbe los picos, reintenta lo que falla y mantiene bajo control cuántas solicitudes golpean la API a la vez.

import json
import os
import time
import urllib.request
import urllib.parse


def sqs_handler(event, context):
    """Process CAPTCHA tasks from SQS queue."""
    api_key = os.environ["CAPTCHAAI_KEY"]
    results = []

    for record in event["Records"]:
        task = json.loads(record["body"])
        try:
            token = solve_captcha(
                api_key,
                task["method"],
                task["params"],
            )
            results.append({
                "task_id": task.get("id"),
                "status": "success",
                "token": token[:50],
            })
        except Exception as e:
            results.append({
                "task_id": task.get("id"),
                "status": "error",
                "error": str(e),
            })

    return {"results": results}

Límites y costes de Lambda que conviene conocer

Antes de llevar esto a producción, ten presentes las cifras que definen el comportamiento de la función. El tiempo de espera y la memoria son las palancas que más importan; el arranque en frío es despreciable frente al tiempo que tarda un CAPTCHA en resolverse.

Factor Valor
Tiempo de espera máximo 15 minutos (configurado en 2 minutos para la mayoría de los CAPTCHA)
Memoria 256 MB suficientes (sin procesamiento pesado)
Simultaneidad Predeterminado 1000 simultáneos (solicita un aumento si lo necesitas)
Arranque en frío ~500 ms para Python (insignificante frente al tiempo de resolución)
Costo ~$0.0001 por solución (solo cálculo)
Dependencias Usa urllib (integrado) para evitar capas Lambda

Errores comunes y cómo resolverlos

Casi todos los fallos en producción caen en cuatro categorías: el tiempo de espera, los permisos de IAM, la latencia del arranque en frío y las dependencias que Lambda no trae de serie.

Problema Causa Solución
La función se agota Tiempo de espera de Lambda < tiempo de resolución Sube el tiempo de espera a 120 s o más
Permiso denegado en el secreto Falta la política de IAM Añade el permiso de lectura de SecretsManager
El arranque en frío añade latencia Invocaciones poco frecuentes Usa simultaneidad aprovisionada
Error de importación de requests No viene incluida en Lambda Usa urllib.request (integrado) o añade una capa

Preguntas frecuentes

¿Cuántos threads de CaptchaAI necesito para la concurrencia de Lambda?

Depende del pico real, no del límite de 1000 invocaciones de Lambda. Cada CAPTCHA en curso ocupa un thread, así que si esperas 15 resoluciones simultáneas, el plan STANDARD ($30/mes, 15 threads) las cubre; para picos mayores, ADVANCE ($90/mes, 50 threads) da margen. Empieza por tu concurrencia observada y escala el plan desde ahí.

¿Cómo evito que un pico de tráfico dispare demasiadas llamadas a la API?

Usa SQS entre la entrada y la función y limita la concurrencia reservada de Lambda a un número acorde a tus threads. Así la cola actúa de amortiguador: las tareas esperan su turno en vez de saturar la API y agotar tus threads de golpe.

¿Qué method y params paso para cada tipo de CAPTCHA?

El handler es agnóstico: reenvía lo que reciba. Para reCAPTCHA usa userrecaptcha con googlekey y pageurl; para Cloudflare Turnstile usa turnstile; para GeeTest v3 usa geetest. Los nombres exactos están en la documentación de la API de CaptchaAI.

¿El arranque en frío ralentiza la resolución?

Apenas. El arranque en frío de Python ronda los 500 ms, un margen insignificante frente a los 10–60 segundos que tarda una resolución típica. Solo si tus invocaciones son muy esporádicas y quieres latencia constante conviene activar la simultaneidad aprovisionada.


Guías relacionadas


Pasa a serverless: obtén tu API key de CaptchaAI hoy.

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