¿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-unauthenticatede 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.secretAccessorque 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.