Rate limiting em APIs: token bucket vs janela fixa
Se a sua API já sofreu com um cliente que dispara milhares de requisições em poucos segundos, você já sabe por que rate limiting existe. A dúvida costuma vir depois: qual algoritmo usar? Aqui a comparação é entre dois dos mais comuns, a janela fixa e o token bucket.
Premissas e limitações deste texto
Antes de entrar no assunto, três avisos.
- Fontes. O material de pesquisa que recebi para este post continha apenas a documentação de
servicesdo Docker Compose, que não trata de rate limiting. Não há, portanto, fonte oficial citada aqui. O conteúdo abaixo vem de conhecimento geral sobre os algoritmos e não traz links nem benchmarks. Confira detalhes de implementação e nomes de parâmetros na documentação da ferramenta que você usa (NGINX, Envoy, API Gateway do seu provedor, biblioteca de Redis etc.). - Código. Os exemplos em Python são ilustrativos e não foram executados neste ambiente. Trate-os como ponto de partida, não como código de produção.
- Cenário. Considere uma API HTTP com clientes identificados por chave, atrás de um ou mais processos de aplicação, com estado de contagem compartilhado.
O problema: limitar sem punir quem se comporta bem
Um limite de taxa precisa responder a duas perguntas. Quantas requisições um cliente pode fazer em média? E quanto de rajada (burst) você tolera?
Os dois algoritmos respondem de formas diferentes, e é aí que aparecem os efeitos práticos.
Janela fixa
Você divide o tempo em intervalos alinhados, por exemplo um minuto, e conta as requisições de cada cliente dentro do intervalo atual. Passou do limite, recebe 429 Too Many Requests. Quando o intervalo vira, o contador zera.
import time
def permitir_janela_fixa(contadores, chave, limite, janela_s):
inicio_janela = int(time.time() // janela_s)
k = (chave, inicio_janela)
contadores[k] = contadores.get(k, 0) + 1
return contadores[k] <= limite
Vantagens: é simples, barato em memória (um contador por cliente por janela) e fácil de explicar ao consumidor da API: "100 requisições por minuto".
Problema clássico: o burst na borda. Com limite de 100 por minuto, um cliente pode enviar 100 requisições no último segundo de uma janela e mais 100 no primeiro segundo da próxima. Em dois segundos, 200 requisições passaram, o dobro do que o limite sugere. Se o backend foi dimensionado para 100 por minuto distribuídos, isso dói.
Token bucket
Cada cliente tem um balde com capacidade máxima de capacidade tokens. Tokens são repostos a uma taxa constante. Cada requisição consome um token. Sem token, a requisição é rejeitada (ou atrasada, se você optar por enfileirar).
import time
def permitir_token_bucket(estado, chave, capacidade, taxa_por_s):
agora = time.monotonic()
tokens, ultimo = estado.get(chave, (capacidade, agora))
tokens = min(capacidade, tokens + (agora - ultimo) * taxa_por_s)
if tokens >= 1:
estado[chave] = (tokens - 1, agora)
return True
estado[chave] = (tokens, agora)
return False
Dois parâmetros controlam o comportamento:
taxa_por_sdefine a média sustentada.capacidadedefine a rajada máxima tolerada.
Isso separa os dois conceitos que a janela fixa mistura. Você pode permitir 10 requisições por segundo em média e aceitar rajadas de até 50, sem que o backend receba mais que isso de uma vez.
Custo: o estado por cliente é maior (tokens e timestamp), e em ambiente distribuído a atualização precisa ser atômica. Um GET seguido de SET em um armazenamento compartilhado gera condição de corrida. Em Redis, por exemplo, isso costuma ser resolvido com script Lua ou operações atômicas equivalentes; confirme na documentação do seu armazenamento.
Comparação prática
| Critério | Janela fixa | Token bucket |
|---|---|---|
| Complexidade | Baixa | Média |
| Estado por cliente | Um contador | Tokens + timestamp |
| Rajada na borda | Sim, até ~2x o limite | Limitada pela capacidade |
| Controle de rajada | Indireto | Explícito |
| Comunicação do limite | Direta ("N por minuto") | Exige explicar taxa e rajada |
Em qual cenário cada um faz sentido
Janela fixa serve bem quando:
- o limite é uma cota de negócio (plano gratuito com 1.000 chamadas por dia), não proteção do backend;
- o backend tolera o dobro do limite na borda;
- você quer a implementação mais simples possível e fácil de depurar.
Token bucket serve melhor quando:
- o objetivo é proteger o backend de picos;
- clientes legítimos fazem rajadas curtas (uma tela que dispara várias chamadas de uma vez) e você não quer bloqueá-los;
- você precisa de previsibilidade sobre a carga máxima instantânea.
Uma combinação comum é usar os dois: janela fixa (ou mensal) para cota comercial e token bucket para proteção de curto prazo. Isso é uma escolha de arquitetura, não uma regra; avalie se a complexidade extra compensa.
Contraponto: nenhum dos dois resolve tudo
Rate limiting por cliente não protege contra uma carga legítima somada que estoura o backend. Para isso você precisa de outros mecanismos, como limites de concorrência, filas e degradação controlada. Também vale definir com cuidado a chave de limitação (IP, chave de API, usuário), porque IP compartilhado por NAT pode punir muita gente por causa de um único cliente barulhento.
Boas práticas na resposta
Independentemente do algoritmo, ajude o cliente a se comportar:
- responda com
429 Too Many Requestsquando o limite for excedido; - informe quando tentar de novo, por exemplo com o cabeçalho
Retry-After; - documente o limite e a política de rajada no contrato da API.
Cabeçalhos como X-RateLimit-* são convenção amplamente usada, mas não uniforme entre provedores. Se for adotá-los, padronize e documente.
Como validar
Antes de colocar em produção, teste o comportamento, não só o código:
- Simule uma rajada no último instante de uma janela e no primeiro da seguinte, e meça quantas requisições passam. Isso mostra o efeito de borda da janela fixa.
- Envie carga constante um pouco acima da taxa configurada e confirme que o excedente recebe
429. - Em ambiente distribuído, rode várias instâncias contra o mesmo estado e verifique se o total aceito respeita o limite (isso revela condições de corrida).
Decisão prática
Se a sua meta é proteger o backend, comece com token bucket e ajuste capacidade conforme a rajada que um cliente legítimo realmente produz. Se é só impor uma cota simples de uso, a janela fixa basta. Em qualquer caso, meça o tráfego real antes de definir os números e confirme na documentação da sua ferramenta como ela implementa o algoritmo escolhido.