Antes de escalar a automação, a conta que importa é uma só: quantos CAPTCHAs cabem em paralelo. No Selenium Grid esse número tem dois tetos — os slots de sessão dos seus nós e as threads do seu plano na CaptchaAI — e vale sempre o menor deles. Quem ignora o segundo sobe vinte contêineres do Chrome e vê as tarefas esperando do lado da API.
Este guia alinha os dois números: Grid do Selenium 4 com Docker Compose, uma chave de API compartilhada, execução paralela em Python e Java e escalonamento no Kubernetes.
Threads da CaptchaAI: o teto real da simultaneidade
A CaptchaAI cobra por thread simultânea, não por resolução: cada plano inclui resoluções ilimitadas dentro das threads contratadas. Uma thread é um CAPTCHA em andamento e, quando ele termina, ela fica livre. É esse número, não a contagem de contêineres, que define o rendimento.
| Plano | Preço/mês | Threads | Grid equivalente |
|---|---|---|---|
| BASIC | $15 | 5 | 1 nó de 5 slots |
| STANDARD | $30 | 15 | 3 nós de 5 slots |
| ADVANCE | $90 | 50 | 10 nós de 5 slots |
| PREMIUM | $170 | 100 | 20 nós de 5 slots |
| CORPORATE | $240 | 150 | 30 nós de 5 slots |
| ENTERPRISE | $300 | 200 | 40 nós de 5 slots |
Regra prática: some o SE_NODE_MAX_SESSIONS de todos os nós e compare com as threads do plano. Slots a mais são navegadores abertos esperando thread livre, ou seja, memória parada; slots a menos são threads pagas e ociosas.
Entram nessa conta reCAPTCHA v2 e v3 (inclusive Enterprise), Cloudflare Turnstile e Challenge, GeeTest v3, imagem/OCR, grade de imagens e BLS, além de CaptchaFox (beta), Friendly Captcha (beta) e Lemin (beta). hCaptcha e FunCaptcha não são suportados; o GeeTest v4 aparece apenas como "em breve".
Arquitetura: um hub, vários nós, uma chave de API
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ 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)
O hub roteia: recebe o pedido de sessão e o encaminha ao nó com slot livre. Os nós não falam com a CaptchaAI — quem chama a API é o código cliente, por HTTP. A chave fica em um lugar só e a integração independe do navegador, então Chrome, Firefox e Edge convivem no mesmo Grid.
Suba o Grid do Selenium 4 com Docker Compose
Um hub e três nós Chrome de 5 sessões: 15 slots, o teto do plano STANDARD (US$ 30/mês, 15 threads).
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
Suba tudo:
docker-compose up -d
Em poucos segundos, http://localhost:4444 mostra os três nós registrados. Nó ausente costuma ser SE_EVENT_BUS_HOST apontando para um nome que o contêiner não resolve.
Um cliente de resolução compartilhado pelos nós
A classe abre a sessão remota, envia o desafio para in.php e consulta res.php a cada 5 segundos até sair de CAPCHA_NOT_READY.
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("--disable-blink-features=AutomationControlled")
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",
"sitekey": 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()
solve_recaptcha_v2 e solve_turnstile diferem só no method e no parâmetro da sitekey (chave pública do widget): googlekey no reCAPTCHA v2, sitekey no Turnstile. Depois, process_task preenche o campo g-recaptcha-response, completa o formulário e envia. O driver.quit() no finally não é opcional: sem ele, uma exceção segura o slot até o SE_SESSION_TIMEOUT estourar e o Grid parece cheio sem estar.
Execução paralela sem estourar o plano
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": "[email protected]"},
"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")
O max_workers deve ficar abaixo do menor valor entre slots livres e threads do plano — aqui, 15 e 15. O timeout=600 existe porque carregar a página, resolver e enviar passa de um minuto em páginas pesadas; sem ele, uma sessão travada segura a thread até o fim do lote.
Cheque a capacidade do Grid antes do lote
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}")
Consultar /status antes de cada execução evita o erro clássico do pipeline agendado: o lote começa com um nó ainda subindo, metade das sessões falha com SessionNotCreated e a culpa sobra para a API. Calcule o max_workers a partir dos slots livres.
Escalonamento automático no 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
Com SE_NODE_MAX_SESSIONS: "3" e o HPA entre 2 e 20 réplicas, a capacidade vai de 6 a 60 slots. Faça a conta do pior caso: 60 slots pedem no mínimo o ADVANCE (US$ 90/mês, 50 threads) e, com folga, o PREMIUM (US$ 170/mês, 100 threads). O maxReplicas é o seu limite de custo.
Integração em Java
Quem já mantém a suíte em JUnit não precisa de um segundo runtime.
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;
}
}
newFixedThreadPool(workers) faz o papel do ThreadPoolExecutor, e future.get(600, TimeUnit.SECONDS) é o mesmo limite por tarefa.
Cenário: regressão noturna de QA em sa-east-1
Um time em São Paulo mantém 300 formulários em staging.example.com e roda a regressão toda madrugada. Hospedar hub e nós em uma região próxima, como sa-east-1, reduz o RTT entre script, Grid e aplicação de teste — a chamada à CaptchaAI é mais uma requisição HTTP no caminho. Com o ADVANCE (US$ 90/mês, 50 threads) e 10 nós de 5 slots, as 300 tarefas saem em lotes de 50; meça o tempo médio no seu próprio ambiente antes de prometer a janela.
Os dados enviados são fictícios e ficam restritos ao staging. Se o pipeline capturar dado pessoal real, as obrigações da LGPD (RGPD, em Portugal) passam a valer sobre esses registros, inclusive sobre o page_source guardado no resultado. E automatize apenas aplicações que você opera ou tem autorização por escrito para testar.
Solução de problemas
| Problema | Causa provável | Como corrigir |
|---|---|---|
SessionNotCreated |
Nenhum slot livre | Mais nós ou SE_NODE_MAX_SESSIONS maior |
| Timeout ao criar a sessão | Nó sem CPU ou memória | Reduza as sessões por nó |
WebDriverException |
Nó saiu do Grid | Retentativa na criação da sessão |
| Contêiner encerrado | Chrome demais por nó | limits de memória e teto de sessões |
| Resolução estourando o tempo | Threads do plano ocupadas | Timeout de polling maior, backoff, plano acima |
| Sessões obsoletas | Limpeza atrasada | Ajuste SE_SESSION_TIMEOUT e garanta driver.quit() |
Perguntas frequentes
Preciso de um plano maior só porque adicionei nós ao Grid?
Depende de quantas sessões esses nós usam ao mesmo tempo: nó ocioso não consome thread. Some os slots que rodam de fato em paralelo e compare com as threads do plano.
O que acontece quando as tarefas passam do número de threads?
As excedentes esperam uma thread ser liberada. Não há cobrança extra por resolução nem taxa por tipo de CAPTCHA — o efeito é só tempo de fila.
hCaptcha e FunCaptcha funcionam nos nós do Grid?
Não. Nenhum dos dois é suportado, e o GeeTest v4 segue apenas como "em breve". Pelo Grid você resolve reCAPTCHA v2/v3, Turnstile, Cloudflare Challenge, GeeTest v3, imagem/OCR, grade de imagens e BLS.
Crio uma sessão por tarefa ou reaproveito a mesma?
Uma por tarefa é o padrão seguro: cookies e estado não vazam entre execuções. Reaproveitar só compensa quando as tarefas compartilham domínio e sessão autenticada.
Guias relacionados
- Como usar Selenium com Python e CaptchaAI
- Interceptação de requisições com Selenium Wire
- Worker threads do Node.js para resolução paralela
Escale a resolução de CAPTCHA entre navegadores distribuídos — obtenha sua chave da CaptchaAI e conecte o Selenium Grid ao seu pipeline.