Se a API de CAPTCHA está fora do ar, insistir em enviar requisições não resolve nada — só queima tempo, thread e créditos do plano. A resposta é um disjuntor (circuit breaker): um componente que percebe a falha, para de chamar o endpoint por um tempo e só volta a testar quando a chance de sucesso é real.
É o mesmo padrão que times de backend usam para proteger integrações com bancos e filas — aqui aplicado ao par in.php / res.php da CaptchaAI. Um worker rodando em sa-east-1 (São Paulo) ganha bastante estabilidade num pico de tráfego só com esse componente na frente das chamadas.
Os três estados de um disjuntor
Um disjuntor de CAPTCHA opera em três estados, sempre nesta ordem.
Fechado
Operação normal: as requisições passam direto para a API da CaptchaAI e as falhas são apenas contadas em segundo plano, sem impacto no fluxo do worker.
Aberto
O limite de falhas configurado (failure_threshold) foi atingido. A partir daí, toda nova requisição é rejeitada na hora, sem sequer chamar o endpoint — o processo para de gastar tempo e créditos do plano numa API que já mostrou estar instável.
Semi-aberto
Depois do período de espera (recovery_timeout), uma única requisição de teste é liberada. Se ela for bem-sucedida, o circuito fecha de novo e o tráfego volta ao normal. Se falhar, o circuito reabre e o cronômetro de espera recomeça do zero.
Implementando o disjuntor em Python
import time
import threading
import requests
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
API_KEY = "YOUR_API_KEY"
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = 0
self.state = "closed" # closed, open, half-open
self._lock = threading.Lock()
def call(self, func, *args, **kwargs):
with self._lock:
if self.state == "open":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "half-open"
print("[circuit] State: half-open — testing one request")
else:
remaining = self.recovery_timeout - (
time.time() - self.last_failure_time
)
raise CircuitOpenError(
f"Circuit open — retry in {remaining:.0f}s"
)
try:
result = func(*args, **kwargs)
with self._lock:
self.failure_count = 0
if self.state == "half-open":
print("[circuit] State: closed — API recovered")
self.state = "closed"
return result
except Exception as e:
with self._lock:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "open"
print(
f"[circuit] State: open — "
f"{self.failure_count} failures"
)
raise
class CircuitOpenError(Exception):
pass
def solve_captcha(sitekey, page_url):
resp = requests.post(SUBMIT_URL, data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}, timeout=15)
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit error: {data['request']}")
task_id = data["request"]
for _ in range(24):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": "1",
}, timeout=15).json()
if poll["status"] == 1:
return poll["request"]
if poll["request"] != "CAPCHA_NOT_READY":
raise Exception(f"Poll error: {poll['request']}")
raise TimeoutError(f"Task {task_id} timed out")
# Usage
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30)
for i in range(10):
try:
token = breaker.call(
solve_captcha, "6Le-SITEKEY", "https://example.com"
)
print(f"[task-{i}] Solved: {token[:40]}...")
except CircuitOpenError as e:
print(f"[task-{i}] Skipped: {e}")
except Exception as e:
print(f"[task-{i}] Failed: {e}")
Resultado esperado:
[task-0] Solved: 03AGdBq26ZfPxL...
[task-1] Solved: 03AGdBq27AbCdE...
[task-2] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-3] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[task-4] Failed: Submit error: ERROR_NO_SLOT_AVAILABLE
[circuit] State: open — 3 failures
[task-5] Skipped: Circuit open — retry in 28s
[task-6] Skipped: Circuit open — retry in 25s
...
[circuit] State: half-open — testing one request
[task-8] Solved: 03AGdBq28FgHiJ...
[circuit] State: closed — API recovered
Dica prática: registre cada transição de estado em log estruturado (como no
failure_thresholderecovery_timeoutobservando os picos reais do seu tráfego, em vez de chutar um valor.
Versão em JavaScript para Node.js
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 60000;
this.failureCount = 0;
this.lastFailureTime = 0;
this.state = 'closed';
}
async call(fn, ...args) {
if (this.state === 'open') {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = 'half-open';
console.log('[circuit] State: half-open');
} else {
const remaining = this.recoveryTimeout - (Date.now() - this.lastFailureTime);
throw new Error(`Circuit open — retry in ${Math.ceil(remaining / 1000)}s`);
}
}
try {
const result = await fn(...args);
this.failureCount = 0;
if (this.state === 'half-open') {
console.log('[circuit] State: closed — recovered');
}
this.state = 'closed';
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.log(`[circuit] State: open — ${this.failureCount} failures`);
}
throw error;
}
}
}
// Usage
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const breaker = new CircuitBreaker({ failureThreshold: 3, recoveryTimeout: 30000 });
async function solveCaptcha(sitekey, pageurl) {
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) throw new Error(submit.data.request);
const taskId = submit.data.request;
for (let i = 0; i < 24; 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: taskId, json: 1 }
});
if (poll.data.status === 1) return poll.data.request;
if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
}
throw new Error('Timeout');
}
(async () => {
for (let i = 0; i < 10; i++) {
try {
const token = await breaker.call(solveCaptcha, '6Le-SITEKEY', 'https://example.com');
console.log(`[task-${i}] Solved: ${token.substring(0, 40)}...`);
} catch (err) {
console.log(`[task-${i}] ${err.message}`);
}
}
})();
Disjuntor e retentativa não são a mesma coisa
Um erro comum é achar que o disjuntor substitui a retentativa (retry) — na prática, um complementa o outro. A retentativa fica dentro do disjuntor; o disjuntor só conta como falha o resultado final, depois que as retentativas se esgotam:
def solve_with_retry(sitekey, page_url, max_retries=2):
for attempt in range(max_retries + 1):
try:
return solve_captcha(sitekey, page_url)
except Exception:
if attempt == max_retries:
raise
time.sleep(2 ** attempt)
# Circuit breaker wraps the retry function
token = breaker.call(solve_with_retry, "6Le-SITEKEY", "https://example.com")
Sem essa camada, um único timeout já contaria como falha no disjuntor e poderia abrir o circuito cedo demais para um problema que uma segunda tentativa resolveria sozinho.
Qual failure_threshold e recovery_timeout usar
Não existe um valor universal — depende do volume de requisições do seu pipeline. A tabela abaixo é um ponto de partida, não uma regra fixa:
| Parâmetro | Tráfego baixo (<10/min) | Tráfego alto (> 100/min) |
|---|---|---|
failure_threshold |
3 | 10 |
recovery_timeout |
30 s | 60 s |
Regra prática: o limite de falhas precisa ser alto o bastante para tolerar erros pontuais, mas baixo o suficiente para parar de martelar uma API com problema. Em pipelines de QA com pouco tráfego, comece pela coluna da esquerda e ajuste conforme os logs; em produção de alto volume, monitore por pelo menos uma semana antes de fechar os valores.
Problemas comuns e como corrigir
Os quatro sintomas abaixo cobrem a maior parte dos tickets de suporte relacionados a disjuntor mal calibrado:
- O circuito abre rápido demais — o limite está muito baixo para o seu volume de tráfego; aumente
failure_threshold. - O circuito nunca volta a fechar —
recovery_timeoutestá muito longo; reduza para 30–60 segundos e reavalie. - Condição de corrida com múltiplos workers — falta trava no estado compartilhado; use
threading.Lock(Python) ou uma operação atômica equivalente. - Todas as requisições bloqueadas numa falha parcial — um único disjuntor está cobrindo todos os endpoints; use disjuntores separados para envio e consulta de resultado.
Perguntas frequentes
Disjuntor substitui a retentativa (retry)?
Não. A retentativa trata falhas passageiras dentro de uma chamada; o disjuntor decide se vale continuar chamando a API depois de falhas repetidas. Use os dois juntos.
Preciso de disjuntores separados para envio e consulta de resultado?
Em escala, sim. in.php pode falhar enquanto res.php segue normal (ou o inverso) — disjuntores independentes dão controle mais fino.
Qual recovery_timeout faz sentido para a API da CaptchaAI?
30 segundos para tráfego baixo, 60 segundos em picos. Se o circuito ficar oscilando entre semi-aberto e aberto, aumente o valor.
O disjuntor funciona com vários workers ao mesmo tempo?
Sim, desde que o estado seja protegido por uma trava — threading.Lock no exemplo em Python. Sem isso, workers concorrentes geram comportamento inconsistente.
O que fazer com as tarefas quando o circuito está aberto?
Coloque a tarefa numa fila para reprocessar depois, mostre uma UI alternativa ou simplesmente pule a operação. Veja Degradação graciosa quando a solução falha.
Resolva CAPTCHA com um pipeline resiliente
Crie sua conta na captchaai.com, pegue sua chave de API e coloque o disjuntor na frente das suas chamadas ainda hoje.