Tutorials

Como limitar a taxa das suas próprias requisições de CAPTCHA

Um loop que não fechou direito no scraper rodou a noite inteira e, pela manhã, o time descobriu que o consumo de threads tinha disparado bem acima do normal — nenhum erro visível, só uma fatura mais alta do que deveria. A CaptchaAI aguenta tranquilamente picos de simultaneidade, mas isso não significa que o seu código deva enviar requisições sem nenhum freio. Este guia implementa três padrões de rate limiting do lado do cliente para a CaptchaAI: token bucket, janela deslizante e limite por orçamento — com código pronto em Python e JavaScript.

Por que aplicar rate limit no seu próprio lado

Mesmo com uma API que escala bem, faz sentido conter a taxa de envio antes que ela saia do seu controle:

  • Bug abre um loop infinito de resolução — sem limite, o loop consome o saldo até travar; com um teto configurado, ele para sozinho.
  • Várias equipes dividem a mesma chave de API — sem limite, o gasto fica sem coordenação; com uma cota por equipe, a divisão é justa.
  • O site de destino bane acima de 100 requisições por minuto — sem controle, a conta acaba bloqueada; ficando abaixo do limite, você nunca esbarra no teto do site.
  • O orçamento mensal é fixo, por exemplo US$ 50 — sem limite, pode estourar numa única tarde; com um teto rígido, o gasto para no valor combinado.

Qual padrão de rate limit escolher

Os três padrões deste guia resolvem problemas ligeiramente diferentes — escolha pelo cenário, não pela ordem em que aparecem abaixo:

  • Token bucket — taxa média estável com espaço para rajadas; complexidade média.
  • Janela deslizante — contar requisições por período fixo, sem rajada; complexidade baixa.
  • Limitador por orçamento — conter custo por dia, semana ou mês; complexidade baixa.
  • Combinado (taxa + orçamento) — a opção mais segura para sistemas em produção; complexidade média.

Se sua aplicação já está em produção, o padrão combinado costuma valer o esforço extra: token bucket ou janela deslizante contém picos de curto prazo, e o teto de orçamento contém o gasto acumulado ao longo do dia.

Padrão 1: token bucket (balde de tokens)

O token bucket libera rajadas controladas sem perder o controle da taxa média: os tokens são repostos numa taxa fixa, e cada requisição de envio consome um token. Quando o balde esvazia, a próxima chamada espera até haver token disponível de novo.

# token_bucket_solver.py
import os
import time
import threading
import requests

API_KEY = os.environ.get("CAPTCHAAI_KEY", "YOUR_API_KEY")

class TokenBucket:
    """Token bucket rate limiter."""

    def __init__(self, rate, capacity):
        """
        rate: tokens added per second
        capacity: max tokens (burst size)
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity
        self.last_refill = time.monotonic()
        self.lock = threading.Lock()

    def acquire(self, timeout=30):
        """Wait for a token. Returns True if acquired, False on timeout."""
        deadline = time.monotonic() + timeout
        while True:
            with self.lock:
                self._refill()
                if self.tokens >= 1:
                    self.tokens -= 1
                    return True

            if time.monotonic() >= deadline:
                return False
            time.sleep(0.1)

    def _refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
        self.last_refill = now

# Allow 10 solves/minute with burst of 5
limiter = TokenBucket(rate=10/60, capacity=5)

def solve_rate_limited(sitekey, pageurl):
    """Solve with rate limiting."""
    if not limiter.acquire(timeout=60):
        raise Exception("Rate limit: could not acquire token within 60s")

    session = requests.Session()
    resp = session.get("https://ocr.captchaai.com/in.php", params={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": "1",
    })
    result = resp.json()

    if result.get("status") != 1:
        raise Exception(f"Submit failed: {result.get('request')}")

    task_id = result["request"]
    time.sleep(15)

    for _ in range(25):
        poll = session.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get",
            "id": task_id, "json": "1",
        })
        poll_result = poll.json()
        if poll_result.get("status") == 1:
            return poll_result["request"]
        if poll_result.get("request") != "CAPCHA_NOT_READY":
            raise Exception(f"Error: {poll_result.get('request')}")
        time.sleep(5)

    raise Exception("Timeout")

Padrão 2: janela deslizante (sliding window)

A janela deslizante conta quantas requisições saíram nos últimos N segundos e bloqueia novas chamadas quando esse total bate no teto. É mais simples de implementar que o token bucket, mas não separa rajadas curtas de uso constante — as duas contam do mesmo jeito.

// sliding_window_solver.js
const axios = require('axios');

const API_KEY = process.env.CAPTCHAAI_KEY || 'YOUR_API_KEY';

class SlidingWindowLimiter {
  constructor(maxRequests, windowMs) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.timestamps = [];
  }

  async acquire(timeoutMs = 60000) {
    const deadline = Date.now() + timeoutMs;

    while (Date.now() < deadline) {
      // Remove expired timestamps
      const cutoff = Date.now() - this.windowMs;
      this.timestamps = this.timestamps.filter(t => t > cutoff);

      if (this.timestamps.length < this.maxRequests) {
        this.timestamps.push(Date.now());
        return true;
      }

      // Wait until the oldest request exits the window
      const waitMs = Math.min(
        this.timestamps[0] + this.windowMs - Date.now() + 10,
        deadline - Date.now()
      );
      if (waitMs > 0) await new Promise(r => setTimeout(r, waitMs));
    }
    return false;
  }
}

// Allow 20 solves per 5 minutes
const limiter = new SlidingWindowLimiter(20, 5 * 60 * 1000);

async function solveRateLimited(sitekey, pageurl) {
  const acquired = await limiter.acquire(60000);
  if (!acquired) throw new Error('Rate limit exceeded');

  const submit = await axios.get('https://ocr.captchaai.com/in.php', {
    params: {
      key: API_KEY, method: 'userrecaptcha',
      googlekey: sitekey, pageurl, json: '1',
    },
  });

  if (submit.data.status !== 1) throw new Error(submit.data.request);
  await new Promise(r => setTimeout(r, 15000));

  for (let i = 0; i < 25; i++) {
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: submit.data.request, json: '1' },
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') throw new Error(poll.data.request);
    await new Promise(r => setTimeout(r, 5000));
  }
  throw new Error('Timeout');
}

Padrão 3: limitador por orçamento

Em vez de contar requisições, esse padrão conta dinheiro: você define um teto de gasto diário e para de enviar novas tarefas assim que ele é atingido.

# budget_limiter.py
import os
import time
from datetime import date

class BudgetLimiter:
    """Limit daily CAPTCHA spending."""

    def __init__(self, daily_budget, cost_per_solve=0.003):
        self.daily_budget = daily_budget
        self.cost_per_solve = cost_per_solve
        self.daily_spend = 0.0
        self.current_date = date.today()

    def can_solve(self):
        """Check if budget allows another solve."""
        if date.today() != self.current_date:
            self.daily_spend = 0.0
            self.current_date = date.today()

        return self.daily_spend + self.cost_per_solve <= self.daily_budget

    def record_solve(self):
        """Record a successful solve against the budget."""
        self.daily_spend += self.cost_per_solve

    @property
    def remaining_budget(self):
        return max(0, self.daily_budget - self.daily_spend)

    @property
    def remaining_solves(self):
        return int(self.remaining_budget / self.cost_per_solve)

# $5/day budget
budget = BudgetLimiter(daily_budget=5.00, cost_per_solve=0.003)

def solve_with_budget(sitekey, pageurl):
    if not budget.can_solve():
        raise Exception(
            f"Daily budget exhausted. Remaining: ${budget.remaining_budget:.2f}"
        )

    # ... solve logic ...
    token = "..."  # actual solve
    budget.record_solve()
    return token

A CaptchaAI cobra por thread simultânea contratada, não por CAPTCHA resolvido — cada plano inclui solves ilimitados dentro das threads contratadas. Um limitador de orçamento não substitui esse modelo; ele resolve outro problema: se um bug dispara chamadas muito acima do que o plano suporta, elas não geram cobrança extra, mas empilham na fila e voltam como timeout. No plano ADVANCE (US$ 90/mês, 50 threads), 300 requisições simultâneas de repente não custam mais caro — só travam até a fila esvaziar.

Dica: trate o limitador de orçamento como uma rede de segurança, não como o controle principal de custo. O controle principal já está no plano de threads contratado; o orçamento só existe para conter o que escapar de um bug.

Se duas ou mais equipes usam a mesma chave de API, é comum estourar o orçamento sem perceber. Persista o contador (de gasto ou de tokens) em Redis ou em um banco compartilhado, nunca só na memória do processo, para que todos os serviços leiam o mesmo estado. Se você também precisa desses registros para auditoria interna, trate-os como dado operacional — serviço, timestamp, custo — sem misturar dados de clientes no mesmo log; isso evita dor de cabeça com a LGPD mais adiante.

Erros comuns ao aplicar rate limit

Problema Causa Correção
Todas as requisições ficam na fila e nada roda Taxa configurada abaixo da demanda real Aumente a taxa ou o tamanho da janela
O limitador de orçamento zera no meio do dia Reinício do processo ou mudança no relógio do sistema Persista o gasto diário em arquivo ou banco
O token bucket esvazia numa única rajada Capacidade pequena demais para o workflow Aumente o parâmetro de capacidade
O rate limit também trava o polling Limitador aplicado ao polling por engano Limite só o envio (in.php), nunca o polling (res.php)

Perguntas frequentes

Aplico rate limit no envio ou no polling?

Só no envio (in.php). O polling (res.php) não cria tarefa nova nem tem custo associado — limitar o polling só atrasa a leitura do resultado, sem economizar nada. Os três padrões deste guia devem envolver apenas a chamada de envio, como no código acima.

Preciso de rate limit mesmo num plano com muitas threads, como o ENTERPRISE (US$ 300/mês, 200 threads)?

Sim. O plano define quantas threads simultâneas você pode usar, não uma proteção contra bugs no seu código. Um loop mal fechado consome threads do mesmo jeito — só que mais rápido quanto maior o plano. O rate limiting evita que esse tipo de falha vire fila travada ou onda de timeouts.

O rate limit do meu lado reduz a taxa de sucesso da CaptchaAI?

Não. Ele só espaça as chamadas de envio no tempo. A taxa de resolução depende do tipo de CAPTCHA e das condições da página, não da velocidade com que você envia as tarefas.

Como evito que duas equipes estourem o mesmo orçamento sem perceber?

Sempre que possível, use uma chave de API por equipe — facilita atribuir custo e não depende de coordenação em tempo real. Quando isso não for viável, use um limitador compartilhado apoiado em Redis (redis-rate-limiter em Python, rate-limiter-flexible em Node.js) e registre o consumo por serviço nos seus próprios logs.

Um rate limit muito agressivo pode derrubar chamadas importantes?

Só se o timeout do acquire() for menor do que sua aplicação consegue esperar. Trate uma chamada que estourou o timeout do limitador como uma retentativa, não como falha da CaptchaAI: aumente a espera, revise a taxa configurada ou separe as tarefas urgentes numa fila com prioridade própria.

Continue aprendendo

Coloque o rate limiting em produção — gere sua chave de API da CaptchaAI e escolha o padrão que combina com o seu volume.

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