La forma de evitar que tus workers distribuidos se pisen al resolver CAPTCHA es darles un estado de sesión compartido: un almacén central (Redis) donde guardan y leen cookies, tokens y asignaciones de proxy. Sin ese estado común, cada worker abre su propia sesión, con sus cookies, su IP y su fingerprint, y el sitio de destino ve un usuario incoherente. Esta guía muestra cómo montar ese almacén en Python y Node.js con la API de CaptchaAI.
Por qué la sesión se rompe al escalar
El síntoma aparece en cuanto pasas de un proceso a varios. Cada worker arranca en frío, resuelve su propio CAPTCHA y obtiene una cookie distinta, así que ninguno puede reutilizar el trabajo del otro:
Worker 1 → Login → Solve CAPTCHA → Get cookie A → Submit form ✅
Worker 2 → New session → Solve CAPTCHA → Get cookie B → Submit form ✅
Worker 3 → Reuse cookie A? → Cookie expired → Solve CAPTCHA → Fail ❌
Sin estado compartido, los workers malgastan resoluciones en sesiones caducadas y generan un comportamiento inconsistente que el sitio de destino puede detectar. El objetivo es que todo el clúster se presente como un puñado de sesiones coherentes, no como decenas de visitantes desconectados entre sí.
Arquitectura: Redis como almacén central
Redis actúa como punto único donde todos los workers publican y consultan el estado. Cada tipo de dato usa la estructura que mejor le encaja: un hash para las cookies por dominio, una lista para los tokens por sitekey, un set para el pool de proxies y una clave con TTL para los locks de sesión.
┌──────────────────────────────────────┐
│ Session State Store │
│ (Redis) │
│ │
│ cookies:{domain} → Hash │
│ tokens:{sitekey} → List │
│ proxies:pool → Set │
│ locks:{domain}:{worker} → String │
└─────┬──────────┬──────────┬──────────┘
│ │ │
┌───▼───┐ ┌──▼────┐ ┌──▼────┐
│Worker1│ │Worker2│ │Worker3│
└───────┘ └───────┘ └───────┘
Qué datos viven en el almacén compartido
No todo se comparte igual. Cada pieza tiene una vida útil distinta y una estrategia propia dentro de Redis:
| Componente del estado | Duración | Estrategia para compartirlo |
|---|---|---|
| Cookies de autenticación | De minutos a horas | Redis con TTL |
| Tokens de CAPTCHA | 90–300 segundos | Lista de Redis (TTL corto) |
Cookie qa_validation_cookie |
~30 minutos | Hash de Redis |
| Tokens CSRF | Por carga de página | No compartir: cada worker genera el suyo |
| Fingerprint del navegador | Permanente | Configuración, no estado en ejecución |
| Asignación de proxy | Por sesión | Pool de proxies respaldado por Redis |
La regla práctica: comparte lo que tiene vida larga y coste alto de regenerar (cookies de login, tokens dentro de su ventana de validez) y deja local lo que es efímero o ligado a una petición concreta (tokens CSRF).
Cuatro patrones según lo que exija el sitio
Antes de escribir código, decide qué necesitas compartir. No siempre hacen falta cookies: para resolución sin estado basta con repartir tokens.
| Patrón | Cuándo usarlo |
|---|---|
| Bloqueo de sesión | Un worker gestiona el login y el resto consume las cookies |
| Pool de tokens | Alto rendimiento: reutiliza tokens dentro de su TTL entre varios workers |
| Compartir cookies | Los workers necesitan sesiones autenticadas |
| Afinidad de proxy | El sitio de destino vincula la IP con la sesión |
Implementación en Python
El almacén de sesión
La clase SessionStore encapsula todas las operaciones sobre Redis: guardar y leer cookies, cachear tokens resueltos con un TTL corto y coordinar el acceso mediante un lock por dominio.
import os
import json
import time
import redis
import requests
from datetime import datetime, timezone
r = redis.Redis(
host=os.environ.get("REDIS_HOST", "localhost"),
port=int(os.environ.get("REDIS_PORT", 6379)),
decode_responses=True
)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
class SessionStore:
"""Shared session state across distributed workers."""
def __init__(self, domain):
self.domain = domain
self.cookie_key = f"session:cookies:{domain}"
self.token_key = f"session:tokens:{domain}"
def save_cookies(self, cookies, ttl=1800):
"""Store cookies from a successful session."""
cookie_data = {name: value for name, value in cookies.items()}
r.hset(self.cookie_key, mapping=cookie_data)
r.expire(self.cookie_key, ttl)
def get_cookies(self):
"""Retrieve shared cookies."""
cookies = r.hgetall(self.cookie_key)
return cookies if cookies else None
def save_token(self, sitekey, token, ttl=80):
"""Store a solved CAPTCHA token."""
key = f"{self.token_key}:{sitekey}"
r.rpush(key, token)
r.expire(key, ttl)
def get_token(self, sitekey):
"""Pop a cached CAPTCHA token."""
key = f"{self.token_key}:{sitekey}"
return r.lpop(key)
def acquire_session_lock(self, worker_id, ttl=300):
"""Ensure only one worker manages the session at a time."""
lock_key = f"session:lock:{self.domain}"
return r.set(lock_key, worker_id, nx=True, ex=ttl)
def release_session_lock(self, worker_id):
"""Release session lock if this worker holds it."""
lock_key = f"session:lock:{self.domain}"
current = r.get(lock_key)
if current == worker_id:
r.delete(lock_key)
El worker con estado compartido
El worker carga las cookies compartidas antes de trabajar, consulta la caché de tokens antes de gastar una resolución en la API y, al terminar, devuelve las cookies resultantes al almacén para el resto del clúster.
class CaptchaWorker:
def __init__(self, worker_id, domain):
self.worker_id = worker_id
self.store = SessionStore(domain)
self.session = requests.Session()
def setup_session(self):
"""Load shared cookies into this worker's session."""
cookies = self.store.get_cookies()
if cookies:
for name, value in cookies.items():
self.session.cookies.set(name, value)
return True
return False
def solve_captcha(self, sitekey, pageurl):
"""Solve with token cache and session sharing."""
# Check for cached token
cached = self.store.get_token(sitekey)
if cached:
return {"solution": cached, "source": "cache"}
# Solve via CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
return {"error": data.get("request")}
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
token = result["request"]
self.store.save_token(sitekey, token)
return {"solution": token, "source": "api"}
if result.get("request") != "CAPCHA_NOT_READY":
return {"error": result.get("request")}
return {"error": "TIMEOUT"}
def process_page(self, url, sitekey):
"""Full workflow: setup session → solve CAPTCHA → submit."""
# Load shared session
self.setup_session()
# Solve CAPTCHA
result = self.solve_captcha(sitekey, url)
if "error" in result:
return result
# Submit form with token
response = self.session.post(url, data={
"g-recaptcha-response": result["solution"]
})
# Share resulting cookies
self.store.save_cookies(dict(self.session.cookies))
return {"status": response.status_code, "source": result["source"]}
Reparto de proxies entre workers
Si todos los workers salen por la misma IP, el sitio de destino los agrupa y los bloquea juntos. Un pool de proxies respaldado por Redis asigna una salida distinta a cada worker y la devuelve al terminar.
class ProxyPool:
"""Distribute proxies across workers to avoid IP conflicts."""
def __init__(self, proxies):
self.pool_key = "session:proxy_pool"
self.assigned_key = "session:proxy_assigned"
# Initialize pool
for proxy in proxies:
r.sadd(self.pool_key, proxy)
def acquire_proxy(self, worker_id, ttl=600):
"""Assign an unused proxy to a worker."""
# Check if worker already has one
existing = r.hget(self.assigned_key, worker_id)
if existing:
return existing
# Pop from available pool
proxy = r.spop(self.pool_key)
if proxy:
r.hset(self.assigned_key, worker_id, proxy)
r.expire(self.assigned_key, ttl)
return proxy
return None
def release_proxy(self, worker_id):
"""Return proxy to the pool."""
proxy = r.hget(self.assigned_key, worker_id)
if proxy:
r.sadd(self.pool_key, proxy)
r.hdel(self.assigned_key, worker_id)
Implementación en Node.js
La misma lógica en Node.js con ioredis y axios: el SessionStore gestiona cookies, tokens y locks, y workerSolve reutiliza el token cacheado antes de llamar a la API.
const Redis = require("ioredis");
const axios = require("axios");
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
class SessionStore {
constructor(domain) {
this.domain = domain;
this.cookieKey = `session:cookies:${domain}`;
this.tokenKey = `session:tokens:${domain}`;
}
async saveCookies(cookies, ttl = 1800) {
const entries = Object.entries(cookies).flat();
if (entries.length > 0) {
await redis.hset(this.cookieKey, ...entries);
await redis.expire(this.cookieKey, ttl);
}
}
async getCookies() {
return await redis.hgetall(this.cookieKey);
}
async saveToken(sitekey, token, ttl = 80) {
const key = `${this.tokenKey}:${sitekey}`;
await redis.rpush(key, token);
await redis.expire(key, ttl);
}
async getToken(sitekey) {
return await redis.lpop(`${this.tokenKey}:${sitekey}`);
}
async acquireLock(workerId, ttl = 300) {
const result = await redis.set(`session:lock:${this.domain}`, workerId, "NX", "EX", ttl);
return result === "OK";
}
async releaseLock(workerId) {
const current = await redis.get(`session:lock:${this.domain}`);
if (current === workerId) await redis.del(`session:lock:${this.domain}`);
}
}
async function workerSolve(store, sitekey, pageurl) {
const cached = await store.getToken(sitekey);
if (cached) return { solution: cached, source: "cache" };
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) return { error: submit.data.request };
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
await store.saveToken(sitekey, poll.data.request);
return { solution: poll.data.request, source: "api" };
}
if (poll.data.request !== "CAPCHA_NOT_READY") return { error: poll.data.request };
}
return { error: "TIMEOUT" };
}
Un caso práctico: monitoreo distribuido de precios
Supón que monitorizas precios en varios marketplaces de la región (una plataforma tipo MercadoLibre, Amazon.es) desde un clúster de 20 workers, y algunas páginas están protegidas por reCAPTCHA v2. Si cada worker resuelve por su cuenta, duplicas llamadas a la API y presentas 20 sesiones sin relación. Con estado compartido, un worker resuelve, guarda el token en Redis con TTL corto y el resto lo reutiliza dentro de su ventana de validez; el pool de proxies evita que las 20 IPs choquen.
En coste, esto encaja con el modelo por threads de CaptchaAI: un plan ADVANCE ($90/mes, 50 threads) cubre 50 resoluciones concurrentes con solves ilimitados, holgado para 20 workers. Recuerda respetar los términos de servicio del sitio y la normativa de protección de datos aplicable (GDPR y LOPDGDD en España, LFPDPPP en México).
Resolución de problemas
| Síntoma | Causa | Solución |
|---|---|---|
| Cada worker acaba con una sesión distinta | Las cookies no se comparten por Redis | Comprueba que se llama a save_cookies tras cada solicitud correcta |
| El token caduca antes de que otro worker lo use | TTL demasiado largo o latencia de red | Reduce el margen del TTL; usa el token en los 10 segundos siguientes a recuperarlo |
| El bloqueo de sesión no se libera nunca | El worker se cayó | El TTL del lock lo libera solo (300 segundos por defecto) |
| El sitio de destino bloquea a los workers | Todos usan el mismo proxy | Usa un pool de proxies con afinidad por worker |
Preguntas frecuentes
¿Cuántos workers puedo ejecutar en paralelo?
Tantos como threads tenga tu plan. En CaptchaAI facturas por thread (un CAPTCHA en curso), no por resolución: el plan BASIC ($15/mes) incluye 5 threads y ADVANCE ($90/mes) llega a 50. El estado compartido reduce el número de resoluciones reales, así que un plan modesto rinde más de lo que su número de threads sugiere.
¿Redis es obligatorio o puedo usar otra base de datos?
Redis es la opción natural por su TTL nativo y sus estructuras (hash, lista, set) que encajan con cada tipo de estado. Cualquier almacén con expiración por clave sirve, pero perderás los locks atómicos y el SPOP del pool de proxies sin escribir esa lógica a mano.
¿Cómo evito condiciones de carrera cuando dos workers escriben cookies a la vez?
Usa el bloqueo de sesión: acquire_session_lock con nx=True deja que solo un worker reautentique y refresque las cookies a la vez. El resto espera y lee el estado ya actualizado, sin sobrescribirlo.
¿Qué pasa si un worker se cae con el bloqueo tomado?
Nada permanente. El lock lleva un TTL (300 segundos por defecto), así que expira solo y otro worker puede adquirirlo. Por eso nunca conviene un lock sin caducidad: un proceso caído dejaría el dominio bloqueado para siempre.
Coordina tus workers CAPTCHA distribuidos sin duplicar resoluciones: obtén tu API key de CaptchaAI.
Guías relacionadas: