¿Cómo resuelves decenas de miles de CAPTCHA sin que un único proceso se convierta en el cuello de botella? La respuesta práctica es repartir el trabajo: una cola central acumula las tareas y un grupo de pods worker las consume en paralelo, creciendo o encogiéndose según cuántas quedan pendientes. Kubernetes aporta justo eso —autoescalado y autorreparación— y CaptchaAI resuelve cada CAPTCHA detrás de una sola API. En esta guía montamos ese sistema paso a paso: workers que sacan tareas de una cola de Redis, un HPA que reacciona a la profundidad de la cola y un productor que encola trabajo desde tu backend.
Este patrón brilla cuando la carga llega en ráfagas. Piensa en una agencia de datos en Madrid o Ciudad de México que procesa de un día para otro la cola de un scraping autorizado, o en un equipo que valida el flujo de checkout de su propia tienda antes de un lanzamiento. El volumen no es constante: pasa de casi cero a miles de tareas por hora y vuelve a bajar. Mantener una flota fija encendida las 24 horas no tiene sentido; escalar pods bajo demanda, sí.
Arquitectura para resolver CAPTCHA a escala
El diseño tiene tres piezas y un único punto de entrada a CaptchaAI:
Producer → Redis Queue → Worker Pods (auto-scaled) → CaptchaAI API
↓
Results Store (Redis)
El productor solo escribe en Redis y no sabe nada de los workers. Los workers tampoco se conocen entre sí: cada uno hace blpop sobre la misma lista, así que Redis reparte las tareas de forma natural y añadir un pod aumenta el ritmo de consumo. Los resultados vuelven a Redis en un hash, desde donde el productor los recoge por su id.
Despliegue de los pods worker
Arrancamos con tres réplicas y límites de recursos ajustados: cada worker pasa la mayor parte del tiempo esperando la respuesta de la API, no quemando CPU, así que 128–256 Mi de memoria y una fracción de vCPU bastan por pod.
# k8s/worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: captcha-worker
labels:
app: captcha-worker
spec:
replicas: 3
selector:
matchLabels:
app: captcha-worker
template:
metadata:
labels:
app: captcha-worker
spec:
containers:
- name: worker
image: your-registry/captcha-worker:latest
env:
- name: CAPTCHAAI_KEY
valueFrom:
secretKeyRef:
name: captchaai-secret
key: api-key
- name: REDIS_URL
value: "redis://redis-service:6379"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
Fíjate en que la API key no aparece en el manifiesto: se inyecta como variable de entorno desde un secreto, que creamos a continuación.
Guardar la API key en un secreto
Nunca metas la clave en la imagen ni en el YAML del Deployment. Kubernetes tiene un objeto pensado justo para esto:
kubectl create secret generic captchaai-secret \
--from-literal=api-key=YOUR_API_KEY
El Deployment ya referencia este secreto con secretKeyRef, de modo que rotar la clave se reduce a recrear el secreto y reiniciar los pods, sin tocar el código ni reconstruir la imagen.
Redis como cola compartida
Redis hace de cola de entrada y de almacén de resultados. Para empezar basta con una réplica; en producción querrás Redis gestionado o con persistencia, pero la mecánica es idéntica:
# k8s/redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "128Mi"
cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
selector:
app: redis
ports:
- port: 6379
El Service expone Redis dentro del clúster en redis-service:6379, exactamente la URL que los workers reciben por REDIS_URL.
El bucle del worker
Aquí está el corazón del sistema. Cada worker se bloquea a la espera de la cola, resuelve la tarea contra CaptchaAI y escribe el resultado con su duración y el nombre del pod que lo procesó —muy útil para depurar después:
# worker.py
import os
import json
import time
import redis
import requests
class CaptchaWorker:
"""Kubernetes worker that processes CAPTCHA tasks from Redis."""
def __init__(self):
self.api_key = os.environ["CAPTCHAAI_KEY"]
self.redis = redis.from_url(
os.environ.get("REDIS_URL", "redis://localhost:6379"),
)
self.base = "https://ocr.captchaai.com"
def run(self):
"""Main worker loop."""
hostname = os.environ.get("HOSTNAME", "unknown")
print(f"Worker {hostname} started")
while True:
result = self.redis.blpop("captcha:queue", timeout=30)
if result is None:
continue
_, raw = result
task = json.loads(raw)
task_id = task.get("id", "unknown")
print(f"[{hostname}] Processing {task_id}")
start = time.time()
try:
token = self._solve(task["method"], task["params"])
duration = time.time() - start
self.redis.hset("captcha:results", task_id, json.dumps({
"status": "success",
"token": token,
"duration": f"{duration:.1f}s",
"worker": hostname,
}))
print(f"[{hostname}] {task_id} solved in {duration:.1f}s")
except Exception as e:
self.redis.hset("captcha:results", task_id, json.dumps({
"status": "error",
"error": str(e),
"worker": hostname,
}))
print(f"[{hostname}] {task_id} failed: {e}")
# Update queue length metric
queue_len = self.redis.llen("captcha:queue")
self.redis.set("captcha:queue_length", queue_len)
def _solve(self, method, params, timeout=120):
resp = requests.post(f"{self.base}/in.php", data={
"key": self.api_key,
"method": method,
"json": 1,
**params,
}, timeout=30)
result = resp.json()
if result.get("status") != 1:
raise RuntimeError(result.get("request"))
captcha_id = result["request"]
start = time.time()
while time.time() - start < timeout:
time.sleep(5)
resp = requests.get(f"{self.base}/res.php", params={
"key": self.api_key,
"action": "get",
"id": captcha_id,
"json": 1,
}, timeout=15)
data = resp.json()
if data["request"] != "CAPCHA_NOT_READY":
if data.get("status") == 1:
return data["request"]
raise RuntimeError(data["request"])
raise TimeoutError("Solve timeout")
if __name__ == "__main__":
CaptchaWorker().run()
El método _solve sigue el flujo estándar de la API de CaptchaAI: envía la tarea a in.php, recibe un id y consulta res.php cada cinco segundos hasta que el token está listo o se agota el timeout. Como CaptchaAI factura por thread concurrente y no por resolución, cada worker puede sondear y reintentar sin coste extra; lo único que consumes es tiempo de thread.
Autoescalado según la profundidad de la cola
Una flota fija desaprovecha las ráfagas y malgasta dinero en los valles. El HorizontalPodAutoscaler resuelve esto escalando por una métrica externa: la longitud de la cola de Redis.
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: captcha-worker-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: captcha-worker
minReplicas: 2
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: redis_queue_length
selector:
matchLabels:
queue: captcha
target:
type: AverageValue
averageValue: "10"
Con averageValue: "10", Kubernetes intenta mantener unas diez tareas pendientes por pod: si la cola crece, añade réplicas hasta veinte; si se vacía, baja hasta dos. Para que esto funcione necesitas un adaptador de métricas (KEDA o Prometheus Adapter) que exponga redis_queue_length al HPA.
Encolar tareas desde el productor
El productor es deliberadamente simple: asigna un id corto a cada tarea, la empuja a la cola y luego recoge los resultados por ese id.
import json
import uuid
import redis
def submit_tasks(redis_url, tasks):
"""Submit CAPTCHA tasks to the queue."""
r = redis.from_url(redis_url)
task_ids = []
for task in tasks:
task_id = str(uuid.uuid4())[:8]
task["id"] = task_id
r.rpush("captcha:queue", json.dumps(task))
task_ids.append(task_id)
return task_ids
def get_results(redis_url, task_ids, timeout=180):
"""Wait for and collect results."""
r = redis.from_url(redis_url)
results = {}
deadline = time.time() + timeout
while len(results) < len(task_ids) and time.time() < deadline:
for tid in task_ids:
if tid in results:
continue
raw = r.hget("captcha:results", tid)
if raw:
results[tid] = json.loads(raw)
time.sleep(1)
return results
Al desacoplar productor y workers encolas diez o diez mil tareas con el mismo código; el HPA dimensiona la flota.
Cuántos threads necesitas para escalar CAPTCHA
El límite real de tu throughput no es Kubernetes, sino cuántos threads incluye tu plan de CaptchaAI. Un thread es un CAPTCHA en vuelo; en cuanto termina, queda libre para el siguiente. La regla práctica es alinear la concurrencia total de tu clúster —pods por resoluciones simultáneas de cada uno— con los threads de tu plan:
- BASIC ($15/mes, 5 threads) para pruebas y volúmenes bajos.
- STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads) para cargas medias con ráfagas.
- PREMIUM ($170/mes, 100 threads) y superiores cuando el
maxReplicasdel HPA empuja de verdad.
Si tus workers piden más resoluciones simultáneas que threads tienes, las solicitudes extra esperan turno: no fallan, pero la latencia sube. Para agencias y freelancers que facturan en monedas locales volátiles, este costo mensual predecible en USD es más fácil de presupuestar que un pago por resolución.
Solución de problemas
| Problema | Causa | Solución |
|---|---|---|
| Los workers no arrancan | Secreto no creado | Ejecuta el comando kubectl create secret |
| Pods en CrashLoopBackOff | Faltan variables de entorno o Redis | Revisa los registros con kubectl logs |
| El HPA no escala | Métricas externas no configuradas | Instala el adaptador de métricas (KEDA) |
| La cola crece pero no se procesa | Workers inactivos o caídos | Revisa el estado del pod y reinícialo |
Preguntas frecuentes
¿Cuántos threads de mi plan necesito para esta flota?
Depende de cuántas resoluciones simultáneas hagan tus pods a la vez. Multiplica el número de réplicas por las resoluciones concurrentes de cada worker y contrata un plan con al menos esos threads; por encima de ese número, las tareas extra simplemente esperan en la cola.
¿Qué pasa con una tarea si un pod se cae a mitad de resolución?
Como el worker usa blpop, la tarea sale de la cola al empezar. Si el pod muere antes de escribir el resultado, esa tarea se pierde salvo que añadas confirmación explícita; para cargas críticas usa un patrón de cola confiable (por ejemplo BLMOVE a una lista de "en proceso") o KEDA con reintentos.
¿Puedo escalar los workers a cero cuando no hay trabajo?
Con el HPA nativo no, porque su mínimo práctico es uno o dos pods. KEDA sí permite escalar a cero y volver a arrancar cuando la cola de Redis recupera tareas, lo que ahorra recursos en cargas intermitentes.
¿Sirve la misma arquitectura para distintos tipos de CAPTCHA?
Sí, porque el worker pasa method y params genéricos a la API. CaptchaAI resuelve reCAPTCHA v2 y v3, Cloudflare Turnstile y GeeTest v3, entre otros tipos compatibles; solo cambias los parámetros de la tarea que encolas, no la arquitectura.
Guías relacionadas
¿Necesitas procesar miles de CAPTCHA al día? Prueba CaptchaAI en tu clúster de Kubernetes.