Tutorials

Token bucket: como limitar a taxa de chamadas à API de CAPTCHA

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.php fica fora do limitador: já tem ritmo próprio com time.sleep(5).
  • Falhas sobem como exceção, e as_completed registra 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:

Os comentários estão desativados para este artigo.