A catraca do estádio
O estádio tem 40 mil lugares. A catraca não existe para maltratar quem chegou por último — existe porque deixar entrar 60 mil não cria 20 mil lugares novos: cria um tumulto em que ninguém assiste ao jogo. Recusar na porta é rápido, previsível e reversível. Descobrir a lotação lá dentro não é nenhuma das três coisas.
Alguém sempre é recusado
A pergunta não é *se* haverá recusa quando a demanda passar da capacidade — é *quem* recusa e *quando*. Sem limite explícito, a recusa acontece lá no fundo, depois de a requisição já ter consumido conexão, CPU e uma consulta ao banco.
É a pior forma de recusar: cara para quem serve e imprevisível para quem chama.
Sob sobrecarga, um sistema sem limite não fica lento de forma uniforme — ele entra em colapso. As filas crescem, a latência sobe, os clientes começam a estourar timeout e a repetir, o que aumenta a carga, o que aumenta a latência. O nome disso é colapso congestivo, e a característica dele é que remover a carga extra não devolve o desempenho de imediato: o sistema fica preso processando trabalho que ninguém mais espera.
Rate limiting corta esse ciclo no ponto mais barato possível: na borda, antes de gastar qualquer recurso caro. Uma requisição recusada em 1ms na entrada custa aproximadamente nada; a mesma requisição recusada depois de 40ms de banco custou uma conexão, uma consulta e uma vaga no pool.
Há uma distinção que vale fixar. Rate limiting protege da *quantidade* de pedidos por unidade de tempo. Bulkhead protege da *concorrência simultânea* de um tipo de pedido. Circuit Breaker protege de insistir com uma dependência quebrada. Os três costumam ser confundidos porque todos recusam trabalho — mas medem coisas diferentes e falham em situações diferentes.
Sobre algoritmos: janela fixa é a implementação mais simples e tem um defeito conhecido na virada — com limite de 100 por minuto, um cliente manda 100 às 10:00:59 e mais 100 às 10:01:00, ou seja, 200 em dois segundos sem violar a regra. Janela deslizante corrige isso ao custo de mais memória. Token bucket costuma ser a melhor escolha padrão: permite rajada até o tamanho do balde e alisa o restante, que é exatamente o comportamento que tráfego real tem.
- Cliente
- requisiçãoIdentifica a chave
- cliente, IP, tenant, rotaTem token disponível?
- sim — consome 1 tokenAplicação
- não → 429 + Retry-AfterResposta
Como isso aparece na prática
O cliente com o laço infinito
Cenário
Uma integração de um cliente tem um bug: em vez de paginar, ela repete a primeira página num laço. Passa a mandar 4 mil requisições por minuto contra uma média histórica de 30, e a latência sobe para todos os outros clientes.
Como o padrão entra
Token bucket por chave de API, com capacidade de 200 e reposição de 20 por segundo. A integração com bug passa a receber 429 com Retry-After, e a plataforma volta ao normal em segundos.
Trade-off: O cliente com bug fica parcialmente quebrado até corrigir o laço — o que é exatamente o objetivo, mas exige um canal de aviso e um limite generoso o bastante para não punir uso legítimo intenso.
O endpoint de envio de e-mail
Cenário
Uma rota de 'recuperar senha' dispara e-mail a cada chamada. Um atacante a usa para bombardear a caixa de entrada de uma vítima, e o provedor de e-mail ameaça suspender a conta por reputação.
Como o padrão entra
Limite por endereço de destino e por IP de origem, bem mais apertado que o limite geral da API, com custo maior por requisição no balde.
Trade-off: Um usuário legítimo que peça recuperação três vezes seguidas é bloqueado por alguns minutos. É um atrito real, aceito porque a alternativa é perder a reputação de envio do domínio inteiro.
Onde isto dá errado
Recusar sem dizer quando voltar
Um
429semRetry-Afterobriga o cliente a adivinhar — e todos adivinham parecido, o que os faz voltar juntos. A resposta de recusa precisa carregar o momento de retorno, e idealmente o limite e o quanto resta.Janela fixa e a rajada na virada
Com limite de 100 por minuto, cabe mandar 100 no último segundo da janela e mais 100 no primeiro da seguinte: 200 em dois segundos, sem violar a regra escrita. Janela deslizante ou token bucket resolvem; janela fixa é a mais fácil de implementar e a mais fácil de furar.
Contar requisição em vez de custo
Um
GETde 200 bytes e um upload de 50 MB contam igual num limitador ingênuo. Quando o custo real varia em ordens de grandeza, o limite precisa ser por peso — tokens proporcionais ao trabalho, não uma unidade por chamada.Limitar por IP num mundo com NAT e proxy
Uma empresa inteira pode sair por um IP só, e uma botnet distribui-se por milhares. Limitar por IP pune escritório e não incomoda atacante. Onde houver identidade — chave de API, conta, sessão —, é ela que deve ser a chave.
Contador local em serviço com várias réplicas
Cada instância mantendo o próprio contador multiplica o limite pelo número de réplicas: dez instâncias com teto de 100 permitem 1000. Ou o estado é compartilhado, ou o teto por instância é dividido — e aí o autoscaling muda o limite sem ninguém perceber.
Limitar o sintoma em vez de corrigir a causa
Racionar acesso a um endpoint lento porque ele derruba o banco esconde o problema real, que é a consulta. O limite compra tempo para corrigir; quando vira solução permanente, o teto só desce com o tempo.
Token bucket com o relógio injetado
// Janela fixa: simples e com um defeito grave na virada.
// Com limite de 100/min, um cliente manda 100 as 10:00:59
// e mais 100 as 10:01:00 — 200 em dois segundos, dentro da regra.
// Token bucket: permite rajada controlada e alisa o resto.
interface Balde {
tokens: number;
/** ultimo instante em que reabastecemos. */
atualizadoEm: number;
}
interface Config {
capacidade: number; // tamanho da rajada permitida
porSegundo: number; // taxa de reposicao
}
// 'agora' entra por parametro: funcao pura, testavel sem relogio.
function permitir(b: Balde, cfg: Config, agora: number, custo = 1): boolean {
const decorrido = (agora - b.atualizadoEm) / 1000;
b.tokens = Math.min(cfg.capacidade, b.tokens + decorrido * cfg.porSegundo);
b.atualizadoEm = agora;
if (b.tokens < custo) return false;
b.tokens -= custo;
return true;
}
// Ao recusar, diga QUANDO voltar. Sem isso o cliente vai adivinhar —
// e adivinhar errado significa todos voltando no mesmo instante.
function cabecalhos(b: Balde, cfg: Config) {
return {
"RateLimit-Limit": String(cfg.capacidade),
"RateLimit-Remaining": String(Math.floor(b.tokens)),
"Retry-After": String(Math.ceil((1 - b.tokens) / cfg.porSegundo)),
};
}import math
class TokenBucket:
"""Rajada ate 'capacidade', reposicao continua a 'por_segundo'."""
def __init__(self, capacidade: int, por_segundo: float):
self.capacidade = capacidade
self.por_segundo = por_segundo
self.tokens = float(capacidade)
self.atualizado_em = 0.0
def permitir(self, agora: float, custo: int = 1) -> bool:
decorrido = agora - self.atualizado_em
self.tokens = min(
self.capacidade, self.tokens + decorrido * self.por_segundo
)
self.atualizado_em = agora
if self.tokens < custo:
return False
self.tokens -= custo
return True
def retry_after(self, custo: int = 1) -> int:
falta = max(0.0, custo - self.tokens)
return math.ceil(falta / self.por_segundo)
# Custo por PESO, nao por requisicao: um upload de 50MB
# nao deveria valer o mesmo que um GET de 200 bytes.
balde = TokenBucket(capacidade=100, por_segundo=10)Quando usar × quando evitar
Use quando
3- APIs públicas e qualquer superfície exposta a clientes que você não controla.
- Recursos caros: e-mail, relatório, chamada a serviço pago por uso.
- Isolamento entre tenants num serviço multiempresa.
Evite quando
3- Entre serviços internos de confiança, onde Bulkhead e fila servem melhor.
- Como remendo permanente para consulta lenta.
- Com contador local quando o serviço tem várias réplicas.
Testar o que ficou
6 armadilhas de Rate limiting viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
O 429 e o Retry-AfterO código HTTP existe só para isto: recusar por excesso de taxa e dizer quando voltar.limit_req do NginxLimitação por taxa embutida no servidor web, antes de a requisição chegar na sua aplicação.Cotas de APITodo provedor sério publica limite por minuto e por chave — é rate limiting virando contrato comercial.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Recusa de propósito quando passa do orçamento.
if (contador > LIMITE) return 429;O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Manter contadores por cliente e janela custa estado, ainda mais quando distribuído entre instâncias
- Um limite mal calibrado ou recusa tráfego legítimo, ou não protege de verdade
Não vale se: o recurso aguenta qualquer pico plausível sem se degradar — limitar aí só adiciona atrito e estado sem proteger nada.
Onde isto já custou caro
Incidentes reais que este conceito explica.
Costuma ser confundido com
Onde isto aparece
Continue por aqui
- BulkheadRecursos compartilhados propagam falha: se todas as chamadas disputam o mesmo pool, a dependência mais lenta consome tudo e derruba as rotas que nem falavam com ela. O anteparo divide o recurso em compartimentos com teto próprio.
- Retry com backoff e jitterFalhas passageiras se resolvem sozinhas se você esperar um pouco e tentar de novo. Mas repetir do jeito errado — sem espera crescente, sem ruído aleatório, sem filtrar o que é repetível — transforma um soluço em uma queda.
- TimeoutTodo pedido que sai da sua máquina precisa de um prazo máximo. Sem ele, a falha mais comum em produção não é o erro — é a espera infinita, que consome recursos até derrubar quem ainda estava saudável.