DevOps y Escalado

Resolver CAPTCHA en Google Cloud Functions con CaptchaAI

¿Necesitas resolver CAPTCHA dentro de un flujo automatizado sin mantener un servidor encendido las 24 horas? Google Cloud Functions te deja desplegar una función que llama a la API de CaptchaAI solo cuando llega trabajo, escala sola con la demanda y no cobra nada mientras está en reposo. En esta guía la montas paso a paso: un endpoint HTTP que resuelve reCAPTCHA v2/v3, Cloudflare Turnstile o GeeTest v3 bajo demanda, con la API key protegida en Secret Manager y un patrón por lotes basado en Pub/Sub para volúmenes altos.


Cuándo compensa el modelo serverless

Antes de escribir código conviene saber si el modelo encaja con tu carga. La ventaja de Cloud Functions frente a una máquina siempre encendida se nota en volúmenes bajos y variables: pagas por milisegundo de ejecución y nada cuando no hay tráfico. A partir de cierto volumen sostenido, una VM fija empieza a igualar el costo. En la práctica, el modelo serverless compensa cuando:

  • El volumen es bajo o irregular, con picos puntuales en lugar de carga constante.
  • Buscas cero gasto en reposo entre lotes de resolución.
  • Prefieres no administrar, parchear ni monitorizar una VM.
Factor Cloud Functions VM siempre encendida
100 resoluciones/día ~$0.01/día ~$1.00/día
1.000 resoluciones/día ~$0.10/día ~$1.00/día
10.000 resoluciones/día ~$1.00/día ~$1.00/día
Costo en reposo $0 Costo completo de la VM
Arranque en frío ~300 ms Ninguno

Para un equipo en Ciudad de México o Madrid que solo procesa unos cientos de resoluciones al día, el modelo de pago por uso cuesta céntimos. A eso se suma el costo de CaptchaAI, que se factura por thread concurrente y no por resolución: el plan BASIC ($15/mes, 5 threads) cubre cargas ligeras y puedes escalar a STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads) cuando el volumen crece. Como ambos costos se cobran en USD, el gasto mensual es predecible aunque factures a tus clientes en pesos o euros.


El endpoint HTTP que resuelve el CAPTCHA

El núcleo del despliegue es una sola función activada por HTTP. Recibe un JSON con el method y los params del CAPTCHA, recupera la API key desde Secret Manager y devuelve el token resuelto. Al apoyarse solo en urllib de la librería estándar evitas dependencias pesadas y recortas el tiempo de arranque en frío, que es el punto donde más se nota la latencia en un entorno serverless.

# main.py
import json
import time
import urllib.request
import urllib.parse
import functions_framework


@functions_framework.http
def solve_captcha(request):
    """HTTP Cloud Function for CAPTCHA solving."""
    # Parse request
    request_json = request.get_json(silent=True)
    if not request_json:
        return json.dumps({"error": "JSON body required"}), 400

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

    # Get API key from Secret Manager
    api_key = _get_secret("captchaai-key")

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


def _get_secret(secret_id):
    """Get secret from GCP Secret Manager."""
    from google.cloud import secretmanager
    client = secretmanager.SecretManagerServiceClient()
    name = f"projects/{_get_project_id()}/secrets/{secret_id}/versions/latest"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("UTF-8")


def _get_project_id():
    """Get current GCP project ID."""
    import urllib.request
    req = urllib.request.Request(
        "http://metadata.google.internal/computeMetadata/v1/project/project-id",
        headers={"Metadata-Flavor": "Google"},
    )
    with urllib.request.urlopen(req) as resp:
        return resp.read().decode()


def _solve(api_key, method, params, timeout=90):
    """Solve CAPTCHA via CaptchaAI API."""
    # Submit
    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
    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 flujo es el de siempre: envías la tarea a in.php, recibes un identificador y consultas res.php cada pocos segundos hasta que el resultado deja de ser CAPCHA_NOT_READY. Como method llega en el cuerpo de la petición, la misma función sirve para varios tipos de CAPTCHA sin tocar el código.


Dependencias

El requirements.txt se mantiene deliberadamente mínimo. Cuantas menos librerías arrastres, más rápido arranca la función en frío, así que aquí solo entra lo imprescindible: el framework de Cloud Functions y el cliente de Secret Manager.

# requirements.txt
functions-framework==3.*
google-cloud-secret-manager==2.*

Desplegar la función

Primero guardas la API key como secreto y luego despliegas con gcloud. Usa --gen2, fija un --timeout holgado (el sondeo puede tardar unos segundos) y limita --max-instances para no escalar sin control ante un pico de tráfico. La prueba con curl confirma que el endpoint responde con el token.

# Create secret
echo -n "YOUR_API_KEY" | gcloud secrets create captchaai-key --data-file=-

# Deploy function
gcloud functions deploy solve-captcha \
  --gen2 \
  --runtime=python311 \
  --region=us-central1 \
  --source=. \
  --entry-point=solve_captcha \
  --trigger-http \
  --allow-unauthenticated \
  --timeout=120s \
  --memory=256MB \
  --max-instances=100

# Test
curl -X POST https://us-central1-PROJECT.cloudfunctions.net/solve-captcha \
  -H "Content-Type: application/json" \
  -d '{
    "method": "userrecaptcha",
    "params": {
      "googlekey": "SITE_KEY",
      "pageurl": "https://example.com"
    }
  }'

Con --allow-unauthenticated el endpoint queda abierto para pruebas; en producción conviene protegerlo (ver la sección de más abajo).


Proteger el endpoint en producción

El despliegue de ejemplo usa --allow-unauthenticated para que puedas probarlo con curl, pero un endpoint abierto que resuelve CAPTCHA no debería quedar expuesto de cara a internet. Antes de pasar a real, cierra el acceso:

  • Redespliega con --no-allow-unauthenticated e invoca la función con un token de identidad de GCP.
  • Coloca API Gateway delante y exige una API key en cada solicitud.
  • Concede a la función solo el rol secretmanager.secretAccessor que necesita, sin permisos de más.

Así el endpoint queda accesible únicamente para tus propios servicios y la API key nunca sale del entorno de ejecución.


Procesamiento por lotes con Pub/Sub

Para cargas grandes, un endpoint HTTP síncrono no es lo ideal: cada resolución mantiene la conexión abierta mientras esperas. La alternativa es desacoplar el trabajo con Pub/Sub. Publicas cada tarea como un mensaje, una función activada por el tema la consume, resuelve el CAPTCHA y publica el token en un tema de resultados. Así procesas miles de tareas en paralelo sin bloquear a quien las encola.

import base64
import json
import functions_framework
from google.cloud import pubsub_v1


@functions_framework.cloud_event
def process_captcha_task(cloud_event):
    """Process CAPTCHA task from Pub/Sub message."""
    data = base64.b64decode(cloud_event.data["message"]["data"])
    task = json.loads(data)

    api_key = _get_secret("captchaai-key")

    try:
        token = _solve(api_key, task["method"], task["params"])
        # Publish result
        publisher = pubsub_v1.PublisherClient()
        topic = f"projects/{_get_project_id()}/topics/captcha-results"
        publisher.publish(topic, json.dumps({
            "task_id": task["id"],
            "status": "success",
            "token": token,
        }).encode())

    except Exception as e:
        print(f"Task {task.get('id')} failed: {e}")

El despliegue cambia el disparador: en lugar de --trigger-http usas --trigger-topic apuntando al tema de entrada.

gcloud functions deploy process-captcha-task \
  --gen2 \
  --runtime=python311 \
  --trigger-topic=captcha-tasks \
  --timeout=120s \
  --memory=256MB

Encolar tareas en Pub/Sub

Del lado del productor solo necesitas publicar un mensaje por cada CAPTCHA que quieras resolver. Cada tarea lleva su id, el method y los params; la función consumidora se encarga del resto.

from google.cloud import pubsub_v1
import json

publisher = pubsub_v1.PublisherClient()
topic = "projects/YOUR_PROJECT/topics/captcha-tasks"

# Submit batch
urls = ["https://site1.com", "https://site2.com", "https://site3.com"]
for i, url in enumerate(urls):
    task = {
        "id": f"task-{i}",
        "method": "userrecaptcha",
        "params": {"googlekey": "SITE_KEY", "pageurl": url},
    }
    publisher.publish(topic, json.dumps(task).encode())
    print(f"Published task-{i}")

Problemas frecuentes y cómo resolverlos

Problema Causa Solución
La función agota el tiempo de espera timeout demasiado corto Ajusta --timeout=120s
Permiso denegado sobre el secreto Falta el rol de IAM Concede secretmanager.secretAccessor
Arranque en frío lento Dependencias demasiado grandes Usa urllib en lugar de requests
Pub/Sub reintenta el mensaje La función devuelve error Devuelve éxito en los errores no reintentables

Preguntas frecuentes

¿Qué tipos de CAPTCHA puedo resolver desde esta función?

Los que soporta CaptchaAI: reCAPTCHA v2 y v3 (incluida la variante Enterprise), Cloudflare Turnstile y Challenge, GeeTest v3, e imágenes/OCR y grid. Solo cambias el method y los params del JSON de entrada. hCaptcha y FunCaptcha no son compatibles por ahora, y GeeTest v4 figura como próximamente.

¿Cómo mantengo baja la factura de GCP?

Aprovecha que Cloud Functions no cobra en reposo y evita dejar instancias mínimas encendidas si no necesitas baja latencia. Si te preocupan los arranques en frío, usa Cloud Scheduler para hacer ping cada pocos minutos en lugar de fijar --min-instances=1, que mantiene una instancia activa y cuesta alrededor de $7/mes.

¿Dónde guardo la API key de forma segura?

En Secret Manager, nunca en el código ni en variables de entorno en claro. La función la recupera en tiempo de ejecución mediante el rol secretmanager.secretAccessor, de modo que la clave no queda en el repositorio ni en los logs de despliegue.

¿Gen2 o Gen1 para resolver CAPTCHA?

Gen2. Admite tiempos de espera más largos (hasta 60 minutos), más memoria y más concurrencia por instancia, justo lo que necesita el sondeo del resultado mientras esperas a que CaptchaAI resuelva el desafío.


Guías relacionadas


Empieza sin servidor en GCP: consigue tu API key de CaptchaAI hoy mismo.

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