A maioria das implementações de rate limit de APIs é trivialmente contornada por rotação ingênua de proxies — mas essa mesma rotação aciona heurísticas antiahuso em minutos. O verdadeiro desafio não é apenas ficar abaixo do limite, mas fazer isso sem parecer um bot. Isso exige entender os dois escopos distintos de rate limit (por IP e por chave), aplicar backoff com jitter, randomizar a rotação do pool e modelar o tempo entre requisições para imitar tráfego orgânico. O padrão token bucket, combinado com um pool de proxies, fornece uma base robusta.
Por IP vs. Por Chave: Os Dois Eixos do Rate Limiting
Os rate limits operam em pelo menos duas dimensões independentes: o IP de origem e a chave da API (ou token). Uma única chave pode permitir 5.000 requisições por hora (limite autenticado do GitHub), mas a mesma chave a partir de um único IP pode ser limitada com um teto de rajada menor. Requisições não autenticadas são ainda mais restritas — tipicamente 60 requisições por hora por IP. Ignorar a dimensão do IP é a maneira mais rápida de acionar uma resposta 429 Too Many Requests (RFC 6585). Um pool de proxies deve distribuir requisições por vários IPs para evitar saturar qualquer origem única, mas cada IP ainda compartilha a mesma chave. Se o limite global da chave é 5.000/hora e você tem 50 proxies, cada proxy só pode fazer 100 requisições por hora antes que o contador no nível da chave dispare. Mapeie ambos os limites antes de escrever uma única linha de código.
Backoff com Jitter: A Diferença entre Polido e Previsível
Backoff exponencial sem jitter é uma impressão digital. Os servidores veem requisições chegando em intervalos perfeitamente dobrados e as marcam como automatizadas. A solução é o jitter completo: sleep(random.uniform(0, min(cap, base * 2 ** attempt))). Isso espalha as tentativas ao longo da janela de tempo, tornando o padrão indistinguível de uma rajada de usuários reais. A própria documentação da AWS sobre backoff exponencial recomenda jitter exatamente por esse motivo. O trecho Python a seguir implementa um token bucket que reabastece a uma taxa fixa, dorme com jitter quando vazio e rotaciona o proxy a cada aquisição bem-sucedida:
import time
import random
from collections import deque
class TokenBucketProxyPool:
def __init__(self, proxies, rate, burst):
self.proxies = deque(proxies)
self.rate = rate # tokens per second
self.burst = burst
self.tokens = burst
self.last_refill = time.monotonic()
def refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
self.last_refill = now
def acquire(self):
while True:
self.refill()
if self.tokens >= 1:
self.tokens -= 1
self.proxies.rotate(1) # simple rotation, but see section below
return self.proxies[0]
else:
sleep_time = (1 - self.tokens) / self.rate
jitter = random.uniform(0, sleep_time * 0.5)
time.sleep(sleep_time + jitter)
# Usage: pool = TokenBucketProxyPool(proxies, rate=2.0, burst=10)
# proxy = pool.acquire() # blocks until token available
Rotação do Pool de Proxies: Randomize, Não Use Round-Robin
Round-robin sequencial por uma lista de proxies é detectável. Um servidor que vê requisições de 1.2.3.4, depois 5.6.7.8, depois 9.10.11.12 em passo sincronizado correlacionará o padrão entre IPs. Em vez disso, escolha proxies aleatoriamente com reposição e adicione um cooldown por proxy. Diretórios públicos de proxies relatam taxas de falha de 60–80% — um proxy que retorna 429 ou um erro de conexão deve ser rebaixado para uma fila de mortos por pelo menos 60 segundos. Implemente uma seleção aleatória ponderada onde proxies recentemente bem-sucedidos têm maior chance de serem escolhidos. O token bucket acima usa um simples rotate(1) para clareza; em produção, substitua por random.choice() e rastreie contagens de falha por proxy.
Modelagem de Requisições: Imite a Média Sem Cruzar a Linha
Mesmo com um token bucket e pool de proxies perfeitos, uma taxa constante de exatamente 2,0 requisições por segundo não é natural. Usuários reais pausam, agrupam e variam os intervalos entre requisições. Adicione um pequeno atraso aleatório (ex.: time.sleep(random.uniform(0.1, 0.5))) antes de cada chamada acquire() e varie a taxa do token bucket em ±10% a cada poucos minutos. No entanto, não adicione atrasos que excedam a janela de rate limit documentada da API — isso anularia o propósito. O objetivo é permanecer dentro do limite enquanto parece ser um conjunto de clientes legítimos. Modelagem excessiva (ex.: inserir atrasos de digitação humanos) é esforço desperdiçado; a API se importa com a frequência das requisições, não com o tempo entre toques no teclado.
Mitigação Ética: Saiba Onde Está a Linha
Toda técnica descrita aqui é uma otimização legítima — até violar os Termos de Serviço da API. A maioria dos ToS proíbe explicitamente "contornar rate limits" ou "usar meios automatizados para acessar o serviço". Ler os ToS não é opcional. Se a API permite 100 requisições por minuto por chave, usar 50 proxies cada um fazendo 2 requisições por minuto está em conformidade. Usar 100 proxies cada um fazendo 100 requisições por minuto não está — é abuso, independentemente de quão inteligente seja seu jitter. A distinção é intenção e volume. Um token bucket combinado com um pool de proxies é uma ferramenta; a mesma ferramenta que raspa um diretório público eticamente também pode ser usada para DDoS em uma API pequena. Documente suas metas de rate limit, audite seus logs em busca de 429s e nunca exceda o teto documentado por chave. Se precisar de mais throughput, peça um limite maior ou pague por um plano dedicado.