Cuando tienes que resolver miles de CAPTCHA por hora, un solo navegador se queda corto: el cuello de botella pasa a ser cuántas sesiones de Chrome puedes tener abiertas a la vez. La respuesta es repartir esas sesiones entre varias máquinas con Selenium Grid y que todas apunten a la misma API key de CaptchaAI.
En esta guía configuras Grid con Selenium 4, conectas CaptchaAI y aplicas patrones de escalado con Docker Compose y Kubernetes, con ejemplos en Python y Java listos para adaptar a tu propio flujo.
Qué necesitas antes de empezar
Antes de levantar el primer nodo, ten a mano estos cuatro elementos:
- Docker y Docker Compose (o un clúster de Kubernetes) para orquestar el hub y los nodos.
- Una cuenta de CaptchaAI con tu API key activa; la misma clave sirve para todo el Grid.
- El sitekey y la URL de cada página protegida que vas a automatizar.
- Máquinas con memoria suficiente: cada instancia de Chrome consume bastante RAM, así que dimensiona los nodos con holgura.
Cómo funciona la arquitectura distribuida
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Test Script │────▶│ Grid Hub │────▶│ Node 1 │
│ (Client) │ │ (Router) │ │ Chrome x 5 │
└─────────────┘ └──────────────┘ └──────────────┘
│ ┌──────────────┐
├─────────────▶│ Node 2 │
│ │ Chrome x 5 │
│ └──────────────┘
│ ┌──────────────┐
└─────────────▶│ Node 3 │
│ Chrome x 5 │
└──────────────┘
All nodes share ──▶ CaptchaAI API (single API key)
El hub actúa como router y asigna cada sesión a un nodo con espacios libres. Como la resolución vive fuera del navegador —la hace la API de CaptchaAI—, una sola credencial alimenta a todo el Grid. Tres ideas sostienen este diseño:
- El hub reparte, los nodos ejecutan. Tú hablas solo con el hub en el puerto
4444; él decide qué nodo atiende cada sesión. - Los nodos son independientes. No se comunican entre sí, así que añades o quitas máquinas sin reconfigurar nada.
- La resolución es central. El token lo entrega la API de CaptchaAI, no el navegador, y por eso el escalado horizontal no multiplica tus credenciales.
Levantar Selenium Grid 4 con Docker
version: "3"
services:
selenium-hub:
image: selenium/hub:4.21.0
container_name: selenium-hub
ports:
- "4442:4442"
- "4443:4443"
- "4444:4444"
chrome-node-1:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
chrome-node-2:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
chrome-node-3:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
docker-compose up -d
Este compose arranca un hub y tres nodos Chrome con SE_NODE_MAX_SESSIONS=5: hasta 15 sesiones simultáneas, con el hub escuchando en el puerto 4444.
Cliente de CaptchaAI para el Grid
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from concurrent.futures import ThreadPoolExecutor, as_completed
class GridCaptchaSolver:
CAPTCHAAI_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, grid_url="http://localhost:4444"):
self.api_key = api_key
self.grid_url = grid_url
def create_session(self):
"""Create a new browser session on the Grid."""
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Remote(
command_executor=self.grid_url,
options=options,
)
return driver
def solve_recaptcha_v2(self, site_url, sitekey):
"""Solve reCAPTCHA v2 via CaptchaAI API."""
# Submit
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": site_url,
"json": 1,
})
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit: {data['request']}")
task_id = data["request"]
# Poll
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] != 1:
raise Exception(f"Solve: {data['request']}")
return data["request"]
raise Exception("Timeout")
def solve_turnstile(self, site_url, sitekey):
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key, "method": "turnstile",
"key": sitekey, "pageurl": site_url, "json": 1,
})
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit: {data['request']}")
task_id = data["request"]
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] != 1:
raise Exception(f"Solve: {data['request']}")
return data["request"]
raise Exception("Timeout")
def process_task(self, task):
"""Process a single CAPTCHA-protected task on a Grid node."""
driver = self.create_session()
try:
driver.get(task["url"])
time.sleep(2)
# Detect sitekey
sitekey = task.get("sitekey")
if not sitekey:
sitekey = driver.execute_script(
"return document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')"
)
if not sitekey:
return {"url": task["url"], "status": "no_captcha", "data": driver.page_source[:500]}
# Solve
token = self.solve_recaptcha_v2(task["url"], sitekey)
# Inject
driver.execute_script(f"""
document.querySelector('#g-recaptcha-response').value = '{token}';
document.querySelectorAll('[name="g-recaptcha-response"]').forEach(
el => el.value = '{token}'
);
""")
# Fill form and submit
if task.get("form_data"):
for field, value in task["form_data"].items():
driver.find_element(By.NAME, field).send_keys(value)
if task.get("submit_selector"):
driver.find_element(By.CSS_SELECTOR, task["submit_selector"]).click()
time.sleep(3)
return {
"url": task["url"],
"status": "success",
"result_url": driver.current_url,
"data": driver.page_source[:1000],
}
except Exception as e:
return {"url": task["url"], "status": "error", "error": str(e)}
finally:
driver.quit()
Cada nodo sigue el mismo patrón de dos pasos de CaptchaAI, encapsulado en la clase GridCaptchaSolver:
- Enviar la tarea a
in.phpcon elsitekeyy la URL de la página. - Sondear el resultado en
res.phpcada cinco segundos hasta que el token esté listo. - Inyectar el token en
g-recaptcha-responsey enviar el formulario, todo dentro del nodo del Grid.
Ejecutar tareas en paralelo
def run_parallel_tasks(api_key, tasks, max_workers=10):
"""Run CAPTCHA tasks in parallel across Grid nodes."""
solver = GridCaptchaSolver(api_key)
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(solver.process_task, task): task
for task in tasks
}
for future in as_completed(futures):
task = futures[future]
try:
result = future.result(timeout=600)
results.append(result)
print(f"[{result['status']}] {result['url']}")
except Exception as e:
results.append({
"url": task["url"],
"status": "exception",
"error": str(e),
})
return results
# Usage
tasks = [
{
"url": "https://site-a.com/form",
"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"form_data": {"name": "Test User", "email": "test@example.com"},
"submit_selector": "#submit",
},
{
"url": "https://site-b.com/register",
"sitekey": "6LdKlZEpAAAAAAOQjzC2v_mJ-",
"form_data": {"username": "testuser"},
"submit_selector": "button[type='submit']",
},
# Add more tasks...
]
results = run_parallel_tasks("YOUR_API_KEY", tasks, max_workers=15)
# Summary
success = sum(1 for r in results if r["status"] == "success")
print(f"\nCompleted: {success}/{len(results)} successful")
El ThreadPoolExecutor lanza cada tarea contra un nodo libre y recoge los resultados a medida que terminan. El timeout=600 por futuro evita que una tarea colgada bloquee al resto del lote.
Cómo dimensionar nodos y threads
Hay dos números que conviene alinear: el max_workers del pool y los threads de tu plan de CaptchaAI. De poco sirve lanzar 50 sesiones de navegador si tu plan solo tiene 15 threads en vuelo, porque las tareas sobrantes esperan en la cola de la API. Como referencia para emparejar el tamaño del Grid con el plan:
- BASIC ($15/mes, 5 threads): pruebas puntuales y desarrollo local con uno o dos nodos.
- ADVANCE ($90/mes, 50 threads): un Grid mediano de QA continuo; margen para saturar varios nodos.
- PREMIUM ($170/mes, 100 threads): lotes grandes y automatización sostenida en varios nodos a la vez.
Por ejemplo, una agencia en Ciudad de México que hace QA de sus formularios de alta empareja un Grid mediano con el plan ADVANCE: cubre su volumen con un costo mensual fijo en USD, no un pago por cada CAPTCHA.
Monitorear el estado del Grid
import requests
def check_grid_status(grid_url="http://localhost:4444"):
"""Check Selenium Grid status and available nodes."""
try:
resp = requests.get(f"{grid_url}/status")
data = resp.json()
nodes = data.get("value", {}).get("nodes", [])
total_slots = 0
available_slots = 0
print(f"Grid Status: {data['value']['ready']}")
print(f"Nodes: {len(nodes)}")
for i, node in enumerate(nodes):
slots = node.get("slots", [])
free = sum(1 for s in slots if not s.get("session"))
total_slots += len(slots)
available_slots += free
print(f" Node {i+1}: {free}/{len(slots)} slots available")
print(f"Total capacity: {available_slots}/{total_slots} available")
return available_slots
except Exception as e:
print(f"Grid check failed: {e}")
return 0
# Adjust workers based on grid capacity
available = check_grid_status()
optimal_workers = min(available, 20)
print(f"Optimal workers: {optimal_workers}")
El endpoint /status del hub te dice cuántos espacios quedan libres por nodo; léelo antes de arrancar el lote y ajusta los workers para evitar errores SessionNotCreated.
Escalado automático con Kubernetes
# selenium-grid-k8s.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: selenium-chrome-node
spec:
replicas: 5
selector:
matchLabels:
app: selenium-chrome
template:
metadata:
labels:
app: selenium-chrome
spec:
containers:
- name: chrome
image: selenium/node-chrome:4.21.0
env:
- name: SE_EVENT_BUS_HOST
value: selenium-hub
- name: SE_EVENT_BUS_PUBLISH_PORT
value: "4442"
- name: SE_EVENT_BUS_SUBSCRIBE_PORT
value: "4443"
- name: SE_NODE_MAX_SESSIONS
value: "3"
resources:
limits:
memory: "2Gi"
cpu: "1"
requests:
memory: "1Gi"
cpu: "500m"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: chrome-node-hpa
spec:
scaleRef:
apiVersion: apps/v1
kind: Deployment
name: selenium-chrome-node
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Cuando la carga sube y baja a lo largo del día, el HorizontalPodAutoscaler mide la CPU y mueve las réplicas entre 2 y 20 según el umbral del 70 %. Los límites de memoria (2Gi por nodo) importan, porque cada Chrome consume bastante. Aunque el Grid crezca, la API key es una sola.
Integración con Java
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.net.URL;
import java.net.http.*;
import java.net.URI;
import java.util.concurrent.*;
public class GridCaptchaSolver {
private final String apiKey;
private final String gridUrl;
private final HttpClient httpClient;
public GridCaptchaSolver(String apiKey, String gridUrl) {
this.apiKey = apiKey;
this.gridUrl = gridUrl;
this.httpClient = HttpClient.newHttpClient();
}
public WebDriver createSession() throws Exception {
ChromeOptions options = new ChromeOptions();
options.addArguments("--no-sandbox", "--window-size=1920,1080");
return new RemoteWebDriver(new URL(gridUrl), options);
}
public List<Map<String, String>> runParallel(
List<Map<String, String>> tasks, int workers
) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(workers);
List<Future<Map<String, String>>> futures = new ArrayList<>();
for (Map<String, String> task : tasks) {
futures.add(executor.submit(() -> processTask(task)));
}
List<Map<String, String>> results = new ArrayList<>();
for (Future<Map<String, String>> future : futures) {
results.add(future.get(600, TimeUnit.SECONDS));
}
executor.shutdown();
return results;
}
}
El patrón es idéntico en Java: un ExecutorService reparte las tareas contra el mismo RemoteWebDriver y usa la misma API de CaptchaAI.
Buenas prácticas en producción
Cuando el Grid deja de ser un experimento y pasa a un flujo real, estos hábitos evitan la mayoría de los sustos:
- Una sesión nueva por tarea. Aísla el estado y evita que cookies o pestañas colgadas contaminen la siguiente resolución.
- Reintentos con retroceso exponencial. Un nodo puede caerse a mitad del lote; reintenta la creación de sesión antes de dar la tarea por perdida.
- Alinea los tiempos de espera.
SE_SESSION_TIMEOUTdel nodo y el tiempo de sondeo del cliente deben superar el peor caso de resolución. - Vigila el saldo y los threads. Escala el plan antes de que la cola de la API se convierta en tu cuello de botella.
- Respeta los términos de servicio y la normativa de protección de datos aplicable en cada sitio que automatices.
Solución de problemas frecuentes
Los fallos más comunes al operar el Grid tienen soluciones directas:
SessionNotCreated— no hay espacios libres: aumenta el número de nodos oSE_NODE_MAX_SESSIONS.- Tiempo de espera en el Grid — nodo sobrecargado: reduce las sesiones simultáneas por nodo.
WebDriverException— nodo desconectado: añade lógica de reintento al crear la sesión.- Memoria agotada — demasiadas instancias de navegador: fija límites de recursos y sesiones máximas.
- Tiempo de espera al resolver el CAPTCHA — API con carga alta: amplía el tiempo de sondeo y añade reintentos.
- Sesiones obsoletas — retraso en la limpieza del Grid: configura
SE_SESSION_TIMEOUT.
Preguntas frecuentes
¿Necesito una API key distinta para cada nodo del Grid?
No. Todo el Grid comparte una sola API key de CaptchaAI; los nodos solo la usan como credencial y añadir más nodos no exige más claves.
¿Cómo ajusto el número de workers a mi plan de CaptchaAI?
Empareja tu concurrencia con los threads del plan: si lanzas más sesiones que threads disponibles, las sobrantes esperan en cola. El plan ADVANCE ($90/mes, 50 threads) da margen para un Grid mediano.
¿Qué pasa si un CAPTCHA tarda más que el tiempo de espera del nodo?
El nodo puede cerrar la sesión antes de recibir el token. Para evitarlo, ajusta dos valores:
SE_SESSION_TIMEOUTen el nodo, para que no cierre la sesión demasiado pronto.- El tiempo de sondeo del cliente, para que espere el peor caso de resolución.
Con ambos alineados y un reintento ante WebDriverException, el lote se recupera solo.
¿Puedo mezclar Firefox o Edge con Chrome en el mismo Grid?
Sí. Añade nodos Firefox o Edge junto a los de Chrome sin cambiar la lógica de resolución: la API de CaptchaAI es independiente del navegador y solo necesita el sitekey y la URL.
Guías relacionadas
- Pipeline de pruebas automatizadas con Python y Selenium
- Selenium Wire + CaptchaAI
- Worker threads de Node.js para resolución en paralelo
Escala la resolución de CAPTCHA en instancias de navegador distribuidas: obtén tu API key de CaptchaAI y despliega tu Selenium Grid.