Quando o pipeline começa a receber ERROR_TOO_MUCH_REQUESTS, o reflexo é reduzir workers — e quase sempre é o ajuste errado. O problema não é quantas tarefas rodam juntas, e sim a velocidade com que elas chegam ao endpoint in.php. Um token bucket resolve isso em cerca de 40 linhas: impõe uma taxa exata ("no máximo 20 envios por segundo") e ainda permite rajadas curtas.
Concorrência não é taxa de requisições
ThreadPoolExecutor(max_workers=30) limita quantas tarefas ficam em andamento, não quantas requisições por segundo saem do processo. No início do lote as duas se confundem: 30 workers disparam 30 envios quase juntos.
- Concorrência — resoluções abertas em paralelo; o teto vem das threads do plano.
- Taxa — envios por segundo; sem limitador, o teto é a sua rede.
- Rajada — envios tolerados de uma vez, quando o crawler acha 20 formulários na mesma página.
O bucket cuida de taxa e rajada; o pool continua cuidando da concorrência.
Como o token bucket funciona
O bucket mantém um saldo de tokens: cada envio consome um, e o saldo é reabastecido em ritmo constante até o teto da capacidade. Com saldo zero, a chamada espera em vez de falhar.
[Bucket] capacity=20, refill=10/sec
Time 0: ████████████████████ 20 tokens available
→ 15 requests consume 15 tokens
Time 0: █████ 5 tokens remain
Time 1s: ███████████████ 15 tokens (5 + 10 refilled)
→ 15 requests consume 15 tokens
Time 1s: (empty) 0 tokens
Time 2s: ██████████ 10 tokens (0 + 10 refilled)
→ Request waits if bucket is empty
- Capacidade — o tamanho máximo da rajada.
- Taxa de recarga — as requisições por segundo sustentadas.
- Espera, não descarte — nenhuma tarefa se perde quando o saldo zera.
A capacidade governa o primeiro segundo; a recarga governa todos os outros. Daí o encaixe com CAPTCHA: a chegada de desafios é irregular, mas o consumo médio precisa ser previsível.
Dimensione a taxa pelas threads do seu plano
Na CaptchaAI a cobrança é por thread simultânea, com resoluções ilimitadas por thread no mês — BASIC (US$ 15/mês, 5 threads), ADVANCE (US$ 90/mês, 50 threads) e ENTERPRISE (US$ 300/mês, 200 threads) são os degraus mais comuns em automação. O limitador não economiza crédito por resolução: ele evita erros, retentativas e latência acumulada.
O teto útil sai de uma conta simples: threads divididas pelo tempo médio de resolução. Com 50 threads e um tempo médio de 20 s medido no seu próprio pipeline, o regime fica perto de 2,5 envios por segundo — enviar 10/s só enfileira trabalho. Use esse número como recarga e trate a capacidade como folga para picos.
Implementação em Python
Bucket thread-safe
O lock protege o saldo contra condições de corrida entre os workers; time.monotonic() impede que um ajuste de relógio estrague a recarga.
import time
import threading
class TokenBucket:
def __init__(self, capacity, refill_rate):
"""
Args:
capacity: Maximum tokens (burst size)
refill_rate: Tokens added per second
"""
self.capacity = capacity
self.refill_rate = refill_rate
self.tokens = capacity
self.last_refill = time.monotonic()
self.lock = threading.Lock()
def acquire(self, timeout=None):
"""Block until a token is available."""
deadline = time.monotonic() + timeout if timeout else float("inf")
while True:
with self.lock:
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return True
# Check timeout
if time.monotonic() >= deadline:
return False
# Wait before retrying (avoid busy loop)
time.sleep(min(1.0 / self.refill_rate, 0.1))
def _refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
new_tokens = elapsed * self.refill_rate
self.tokens = min(self.capacity, self.tokens + new_tokens)
self.last_refill = now
Envio limitado à API da CaptchaAI
Com o bucket pronto, a integração muda uma linha: peça o token antes de chamar in.php.
import os
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Allow 10 submissions/sec with burst of 20
rate_limiter = TokenBucket(capacity=20, refill_rate=10)
def solve_captcha_rate_limited(sitekey, pageurl):
"""Solve with rate limiting on submission."""
# Wait for token before submitting
rate_limiter.acquire()
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:
raise RuntimeError(data.get("request"))
captcha_id = data["request"]
# Polling doesn't need rate limiting (separate concern)
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:
return result["request"]
if result.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(result.get("request"))
raise TimeoutError("Solve timeout")
# Run 100 tasks through rate limiter
tasks = [
{"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"pageurl": f"https://example.com/p/{i}"}
for i in range(100)
]
with ThreadPoolExecutor(max_workers=30) as executor:
futures = {
executor.submit(
solve_captcha_rate_limited, t["sitekey"], t["pageurl"]
): t for t in tasks
}
for future in as_completed(futures):
task = futures[future]
try:
solution = future.result()
print(f"[OK] {task['pageurl']}")
except Exception as e:
print(f"[ERR] {task['pageurl']}: {e}")
- O limitador é global do processo: todos os workers usam a mesma instância.
- A consulta em
res.phpfica fora do limitador: já tem ritmo próprio comtime.sleep(5). - Falhas sobem como exceção, e
as_completedregistra o erro por tarefa sem derrubar o lote.
Implementação em JavaScript
Bucket assíncrono
Em Node.js não há thread para bloquear: o bucket calcula quanto falta para o próximo token e aguarda com setTimeout.
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.refillRate = refillRate; // tokens per second
this.tokens = capacity;
this.lastRefill = Date.now();
this.waitQueue = [];
}
_refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
async acquire() {
this._refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return;
}
// Wait until a token is available
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
await new Promise((resolve) => setTimeout(resolve, waitTime));
this._refill();
this.tokens -= 1;
}
}
Lote com taxa controlada
const axios = require("axios");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const rateLimiter = new TokenBucket(20, 10); // 20 burst, 10/sec sustained
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function solveCaptchaLimited(sitekey, pageurl) {
// Wait for rate limit token
await rateLimiter.acquire();
const submitResp = await axios.post(
"https://ocr.captchaai.com/in.php",
null,
{
params: {
key: API_KEY,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: 1,
},
}
);
if (submitResp.data.status !== 1) {
throw new Error(submitResp.data.request);
}
const captchaId = submitResp.data.request;
for (let i = 0; i < 60; i++) {
await sleep(5000);
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (result.data.status === 1) return result.data.request;
if (result.data.request !== "CAPCHA_NOT_READY") {
throw new Error(result.data.request);
}
}
throw new Error("TIMEOUT");
}
// Solve 100 tasks — rate limiter ensures max 10 submissions/sec
async function batchSolve(tasks) {
const results = await Promise.allSettled(
tasks.map((t) => solveCaptchaLimited(t.sitekey, t.pageurl))
);
const solved = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected").length;
console.log(`Solved: ${solved}, Failed: ${failed}`);
}
Promise.allSettled dispara tudo de uma vez; é o acquire() que espalha os envios ao longo do tempo.
Como escolher capacidade e taxa de recarga
| Carga de trabalho | Capacidade (rajada) | Recarga (sustentada) |
|---|---|---|
| Coleta leve | 5 | 2/s |
| Automação padrão | 20 | 10/s |
| Pipeline de alto volume | 50 | 30/s |
| Throughput máximo | 100 | 50/s |
- Capacidade igual ao dobro da recarga libera rajadas de cerca de 2 segundos.
- Comece conservador e suba acompanhando a taxa de erro.
- Uma chave de API compartilhada por vários serviços soma a taxa de todos.
Cenário: monitoramento autorizado de preços em sa-east-1
Um time em São Paulo roda coleta autorizada com workers em sa-east-1 e valida o fluxo em https://staging.example.com/checkout. Às 9h o agendador libera 4 mil páginas de uma vez e o pipeline encontra centenas de desafios reCAPTCHA v2 em segundos. Sem limitador, o pico vira erro, backoff e um lote que termina mais tarde. Com capacidade 20 e recarga 10/s, os primeiros 20 envios saem na hora e o resto escoa em ritmo constante. Havendo dados pessoais, documente o escopo da coleta para atender à LGPD (RGPD em Portugal).
Token bucket comparado a outros algoritmos
| Algoritmo | Comportamento | Indicado para |
|---|---|---|
| Token bucket | Taxa suave com rajada permitida | Chamadas à API de CAPTCHA |
| Leaky bucket | Vazão de saída fixa, sem rajadas | Limites rígidos de taxa |
| Janela fixa | Contagem por janela, picos na virada | Contadores simples |
| Janela deslizante | Contagem em período contínuo | Controle preciso de taxa |
Para CAPTCHA, o token bucket é o padrão mais adequado: proibir rajadas transforma um pico natural em atraso.
Quando um bucket em memória não basta
Com várias réplicas na mesma chave de API, cada processo tem o próprio saldo e a taxa real vira a soma de todos.
- Mova o saldo para o Redis, com consumo e recarga atômicos.
- Divida a taxa alvo pelo número de réplicas antes de ligar o autoscaling.
- Limite a fila de espera e devolva erro cedo, em vez de acumular tarefas.
Solução de problemas
| Problema | Causa provável | Correção |
|---|---|---|
ERROR_TOO_MUCH_REQUESTS persiste |
Recarga acima do que o pipeline sustenta | Reduza a recarga; confira se outro processo usa a mesma chave |
| Latência alta por envio | Saldo zerado, tarefas esperando recarga | Aumente a capacidade |
| Memória crescendo | Fila de espera acumulando | Defina um tamanho máximo de fila |
| Limite não compartilhado | Bucket só em memória | Use um bucket em Redis |
| Throughput abaixo das threads | Recarga baixa demais | Recalcule com o tempo médio medido |
Perguntas frequentes
O token bucket substitui um circuit breaker?
Não. O bucket controla a velocidade dos envios enquanto tudo funciona; o circuit breaker corta o tráfego quando algo quebra. Em produção os dois convivem.
Quantos envios por segundo o meu plano sustenta?
Divida as threads pelo tempo médio de resolução: com ADVANCE (US$ 90/mês, 50 threads) e um tempo médio medido de 20 s, o regime fica perto de 2,5 envios por segundo. Passar disso só enfileira trabalho.
Limitar a taxa deixa o lote mais lento?
Na prática, não. O tempo total continua governado pelo tempo de resolução e pelas threads disponíveis. Muda só onde a espera acontece: numa fila local, em vez de retentativas depois do erro.
Artigos relacionados
Próximas etapas
Coloque o limitador antes do primeiro envio e meça a taxa real do seu pipeline — crie sua chave de API da CaptchaAI e ajuste capacidade e recarga com dados do seu ambiente.
Guias relacionados: