Un único proceso worker resolviendo CAPTCHA se queda corto en cuanto tu pipeline supera unos cientos de tareas por minuto: las solicitudes se encolan y una sola caída lo detiene todo. La solución es repartir el trabajo entre varios workers detrás de un load balancer, para distribuir la carga, ganar failover automático y escalar horizontalmente sin tocar tus scrapers. Esta guía cubre los patrones para desplegar workers de CaptchaAI detrás de un balanceador: qué algoritmo elegir, cómo configurar NGINX y el servidor worker, y cómo diagnosticar los fallos habituales.
El patrón: workers detrás del balanceador
Tus scrapers no hablan directamente con la API de CaptchaAI: envían cada tarea a un endpoint interno (/solve), el balanceador la reparte entre los workers y es cada worker quien llama a la API, sondea el resultado y lo devuelve. Añadir o retirar workers no toca a los scrapers: solo cambias la lista del upstream.
[Scraper 1] ──┐ ┌── [Worker 1] ──→ CaptchaAI API
[Scraper 2] ──┤── [Load Balancer] ──┤── [Worker 2] ──→ CaptchaAI API
[Scraper 3] ──┘ └── [Worker 3] ──→ CaptchaAI API
Qué algoritmo de balanceo elegir
No todos los repartos rinden igual cuando la duración de las tareas varía tanto:
| Algoritmo | Cuándo usarlo | Riesgo principal |
|---|---|---|
| Round-robin | Todos los workers tienen capacidad y latencia parecidas | Un worker lento sigue recibiendo tráfico al mismo ritmo |
least_conn |
Los tiempos de resolución varían mucho entre tareas | Necesita health checks fiables para no favorecer a workers degradados |
backup |
Quieres capacidad de emergencia sin usarla a diario | El worker de respaldo puede quedar frío si nunca recibe tráfico |
| Sticky / afinidad | El worker conserva estado local o una sesión de navegador | Puede desequilibrar la carga si la afinidad no expira |
Una agencia que monitoriza precios para varios clientes tiene picos irregulares: least_conn evita que un worker atascado en una tarea larga acapare las solicitudes nuevas.
Configuración de NGINX
Reparto round-robin (por defecto)
El punto de partida; el proxy_read_timeout es generoso porque una resolución puede tardar minutos.
upstream captcha_workers {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
}
server {
listen 80;
server_name captcha.internal;
location /solve {
proxy_pass http://captcha_workers;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10s;
proxy_read_timeout 300s; # CAPTCHA solving can take minutes
}
location /health {
proxy_pass http://captcha_workers;
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
}
}
least_conn: el mejor reparto para resolver CAPTCHA
Como la duración es impredecible, enrutar al worker con menos conexiones activas reparte mejor que el round-robin. Sube el weight de los más potentes y añade max_fails.
upstream captcha_workers {
least_conn; # Route to worker with fewest active connections
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080 weight=2; # Higher capacity worker
# Health checks
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}
Workers de respaldo para failover
Un worker backup solo entra cuando los demás caen: tu red de seguridad.
upstream captcha_workers {
least_conn;
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080 backup; # Only used when others are down
}
El servidor worker (API interna)
Cada worker expone dos endpoints: /solve, que envía la tarea y sondea el resultado, y /health, que reporta su carga para sacarlo de rotación cuando se satura.
Python (Flask)
import os
import time
import threading
import requests
from flask import Flask, request, jsonify
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
app = Flask(__name__)
# Track active tasks for load reporting
active_tasks = 0
tasks_lock = threading.Lock()
max_concurrent = int(os.environ.get("MAX_CONCURRENT", "20"))
@app.route("/solve", methods=["POST"])
def solve():
global active_tasks
with tasks_lock:
if active_tasks >= max_concurrent:
return jsonify({"error": "WORKER_AT_CAPACITY"}), 503
active_tasks += 1
try:
data = request.json
result = solve_captcha(data)
return jsonify(result)
finally:
with tasks_lock:
active_tasks -= 1
@app.route("/health")
def health():
with tasks_lock:
load = active_tasks / max_concurrent
return jsonify({
"status": "healthy" if load < 0.9 else "overloaded",
"active_tasks": active_tasks,
"max_concurrent": max_concurrent,
"load_pct": round(load * 100, 1)
}), 200 if load < 0.9 else 503
def solve_captcha(data):
session = requests.Session()
payload = {
"key": API_KEY,
"method": data.get("method", "userrecaptcha"),
"googlekey": data.get("sitekey"),
"pageurl": data.get("pageurl"),
"json": 1
}
if data.get("proxy"):
payload["proxy"] = data["proxy"]
payload["proxytype"] = data.get("proxytype", "HTTP")
resp = session.post("https://ocr.captchaai.com/in.php", data=payload)
result = resp.json()
if result.get("status") != 1:
return {"error": result.get("request")}
captcha_id = result["request"]
for _ in range(60):
time.sleep(5)
poll = session.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": captcha_id, "json": 1
}).json()
if poll.get("status") == 1:
return {"solution": poll["request"], "captcha_id": captcha_id}
if poll.get("request") != "CAPCHA_NOT_READY":
return {"error": poll.get("request")}
return {"error": "TIMEOUT"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, threaded=True)
JavaScript (Express)
const express = require("express");
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT || "20", 10);
const PORT = parseInt(process.env.PORT || "8080", 10);
let activeTasks = 0;
const app = express();
app.use(express.json());
app.post("/solve", async (req, res) => {
if (activeTasks >= MAX_CONCURRENT) {
return res.status(503).json({ error: "WORKER_AT_CAPACITY" });
}
activeTasks++;
try {
const result = await solveCaptcha(req.body);
res.json(result);
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
activeTasks--;
}
});
app.get("/health", (req, res) => {
const load = activeTasks / MAX_CONCURRENT;
const status = load < 0.9 ? "healthy" : "overloaded";
res
.status(load < 0.9 ? 200 : 503)
.json({ status, activeTasks, maxConcurrent: MAX_CONCURRENT, loadPct: Math.round(load * 100) });
});
async function solveCaptcha(data) {
const submitResp = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: {
key: API_KEY,
method: data.method || "userrecaptcha",
googlekey: data.sitekey,
pageurl: data.pageurl,
json: 1,
},
});
if (submitResp.data.status !== 1) {
return { error: submitResp.data.request };
}
const captchaId = submitResp.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (pollResp.data.status === 1) {
return { solution: pollResp.data.request, captchaId };
}
if (pollResp.data.request !== "CAPCHA_NOT_READY") {
return { error: pollResp.data.request };
}
}
return { error: "TIMEOUT" };
}
app.listen(PORT, () => console.log(`Worker listening on port ${PORT}`));
Estrategias de enrutamiento comparadas
Las estrategias habituales de un balanceador y para qué encaja cada una:
| Estrategia | Cómo funciona | Mejor para |
|---|---|---|
| Round-robin | Rotación secuencial | Workers de igual capacidad |
least_conn |
Enruta al menos cargado | Resolución de CAPTCHA (duración variable) |
| Ponderado (weight) | Proporcional al peso asignado | Workers de capacidad mixta |
| IP hash | Mismo cliente → mismo worker | Cuando necesitas afinidad de sesión |
| Aleatorio | Selección al azar | Carga simple y repartida de forma uniforme |
Recomendación: usa least_conn para resolver CAPTCHA. Como la duración varía (de 5 a 120 segundos), el round-robin genera una carga desigual.
Balanceo del lado del cliente
Cuando no puedes montar un balanceador externo, mueve el enrutamiento al cliente. Esta clase envía cada tarea al worker menos cargado y descarta el que responda 503 o falle:
import random
import requests
class ClientLoadBalancer:
def __init__(self, workers):
self.workers = [
{"url": url, "healthy": True, "active": 0}
for url in workers
]
def get_worker(self):
healthy = [w for w in self.workers if w["healthy"]]
if not healthy:
raise Exception("No healthy workers")
return min(healthy, key=lambda w: w["active"])
def solve(self, task):
worker = self.get_worker()
worker["active"] += 1
try:
resp = requests.post(
f"{worker['url']}/solve",
json=task,
timeout=300
)
if resp.status_code == 503:
worker["healthy"] = False
return self.solve(task) # Retry on another worker
return resp.json()
except requests.RequestException:
worker["healthy"] = False
return self.solve(task)
finally:
worker["active"] -= 1
lb = ClientLoadBalancer([
"http://10.0.1.10:8080",
"http://10.0.1.11:8080",
"http://10.0.1.12:8080"
])
result = lb.solve({"sitekey": "6Le-wvkS...", "pageurl": "https://example.com"})
Diagnóstico de problemas comunes
| Síntoma | Causa | Solución |
|---|---|---|
| 502 Bad Gateway | El worker se cayó o no arrancó | Revisa los logs del worker; comprueba el binding del puerto |
| Carga repartida de forma desigual | Round-robin con tareas de duración variable | Cambia a least_conn |
| Health check con falso positivo | El check pasa pero el worker está al límite | Incluye el porcentaje de carga en la respuesta de /health |
| Connection timeout | proxy_read_timeout demasiado corto |
Súbelo a 300 s o más para resolver CAPTCHA |
Preguntas frecuentes
¿Cuántos workers y threads de CaptchaAI necesito para mi volumen?
Depende de tu concurrencia. CaptchaAI factura por thread concurrente (un thread es un CAPTCHA en curso), con resoluciones ilimitadas por thread. Dimensiona primero los threads: ADVANCE ($90/mes, 50 threads) cubre 50 resoluciones simultáneas. Reparte esa concurrencia entre 2 a 4 workers para el failover; más workers no dan throughput extra si no subes los threads del plan.
¿Cómo evito que el balanceador corte las tareas de resolución largas?
Sube los tiempos de espera. Como una resolución puede tardar más de un minuto, configura proxy_read_timeout en 300 s o más en NGINX y usa un timeout equivalente en el cliente. Con los valores por defecto se cortaría antes de tiempo.
¿Debo usar sesiones sticky con workers de CAPTCHA?
No. Las solicitudes de resolución no tienen estado: cualquier worker atiende cualquier tarea, así que la afinidad de sesión solo crearía una distribución desigual.
¿Cómo reparto workers en varias regiones?
Usa un balanceador global (AWS Global Accelerator, Cloudflare Load Balancing) que enrute a la región sana más cercana, con un balanceador local por región para acortar la latencia.
Artículos relacionados
- Manejo de errores en callbacks de CaptchaAI
- Pipeline de pruebas automatizado con CaptchaAI
- Arquitectura de resolución CAPTCHA de alto volumen
Escala tu resolución con CaptchaAI
Reparte tu resolución de CAPTCHA entre varios workers y gana failover. Consigue tu clave API de CaptchaAI y despliégala detrás de un load balancer.
Guías relacionadas: