Uma API multi-tenant que limita requisições por endereço IP está medindo a coisa errada. O IP identifica de onde a chamada veio, não quem vai pagar por ela. Num cliente que roda atrás de um NAT corporativo, cem usuários compartilham um IP e batem no teto de um só. Num cliente que roda em funções serverless, cada invocação sai de um IP diferente e o teto nunca é alcançado. Os dois casos aparecem no mesmo dia, no mesmo produto, e o resultado é o pior possível: reclamação de quem paga e consumo livre de quem abusa.
O problema não é o algoritmo de contagem. É a chave. Antes de discutir janela deslizante contra token bucket, vale decidir o que está sendo contado e por quê.
A chave de rate limit deve espelhar a unidade de cobrança
Em produto SaaS, a regra prática é curta: o limite existe para proteger a plataforma de um tenant e para dar previsibilidade ao tenant. Se o contrato vende 100 mil chamadas por mês para a conta, o contador precisa viver na conta — não no IP, não no token individual, não na sessão.
Isso não significa uma chave só. Um esquema que funciona bem na prática usa três níveis simultâneos, avaliados em ordem, do mais barato ao mais caro de calcular:
- Global por tenant: o teto contratual. Protege a plataforma e é o número que o comercial promete.
- Por credencial dentro do tenant: impede que uma integração mal escrita do cliente consuma sozinha a cota inteira e derrube as outras integrações do mesmo cliente.
- Por rota crítica: endpoints caros — relatórios, exportações, buscas amplas — merecem contador próprio, porque uma chamada dessas custa cem vezes uma leitura simples.
O IP entra num quarto nível, e só para tráfego não autenticado: rotas de login, de recuperação de senha, de cadastro. Ali o IP é o único identificador disponível e o objetivo é outro — conter enumeração e força bruta, não distribuir cota.
tenant_id:credential_id:route_class. Os três componentes já existem no contexto da requisição depois da autenticação; nenhum exige consulta extra ao banco.Nem toda requisição custa o mesmo, e o contador precisa saber disso
Contar requisições trata um GET /health e um POST /reports/export como equivalentes. Não são. O segundo pode ocupar um worker por trinta segundos e varrer milhões de linhas.
A alternativa é contar custo em vez de chamadas: cada rota declara um peso, e o consumo debitado é o peso, não a unidade. Uma leitura simples custa 1, uma busca paginada custa 5, uma exportação custa 50. O cliente continua vendo um número único de saldo, e a plataforma passa a se proteger do que realmente dói.
O custo dessa abordagem é de comunicação, não de implementação. Cota em “unidades” exige documentar a tabela de pesos e mantê-la estável — mudar o peso de uma rota é, na prática, mudar o preço. Vale a pena quando existem endpoints com ordens de grandeza de diferença entre si. Não vale quando todas as rotas são leituras e escritas parecidas: nesse caso, contar requisição é mais simples e igualmente honesto.
Onde o contador vive muda o que ele consegue prometer
Há três lugares plausíveis para guardar o estado do limite, e a escolha determina a precisão possível.
| Local do contador | Precisão | Latência somada | Falha quando |
|---|---|---|---|
| Memória do processo | Baixa: cada réplica conta separado | Desprezível | Escala horizontal — N réplicas viram N vezes o limite |
| Redis compartilhado | Alta | Um round-trip, tipicamente sub-milissegundo na mesma rede | Redis fica indisponível ou vira ponto único |
| API gateway gerenciado | Alta, mas com a granularidade que o gateway oferece | Nenhuma adicional | A chave desejada não cabe no modelo do gateway |
O contador em memória local costuma ser descartado cedo demais. Ele tem um uso legítimo: como primeira barreira, com um teto propositalmente frouxo, para absorver picos absurdos sem sequer tocar o Redis. Um limite local de, digamos, dez vezes o teto real por réplica corta um cliente em laço infinito antes que ele gere carga na infraestrutura de contagem. A contagem exata continua no Redis, uma camada depois.
Sobre o Redis: ele vira dependência crítica do caminho de request. Isso precisa de decisão explícita sobre o que fazer quando ele não responde. Falhar aberto — deixar passar — mantém o produto no ar e abre mão da proteção justamente no momento de instabilidade. Falhar fechado — bloquear tudo — transforma indisponibilidade do Redis em indisponibilidade da API.
A escolha defensável para a maioria dos produtos é falhar aberto no limite por tenant e falhar fechado nas rotas não autenticadas. O tenant pagante não deve ser punido por um problema de infraestrutura; a rota de login não deve ficar desprotegida sob nenhuma circunstância.
Escolha do algoritmo: o único ponto onde janela fixa realmente atrapalha
Janela fixa conta requisições em blocos de tempo alinhados ao relógio — de 10:00:00 a 10:00:59, depois zera. É trivial de implementar com INCR e EXPIRE, e tem um defeito conhecido: um cliente pode gastar a cota inteira nos últimos segundos de uma janela e a cota inteira nos primeiros da seguinte, produzindo o dobro do teto em um intervalo curto.
Se o limite existe para faturamento, esse defeito é irrelevante — no fim do mês o total está certo. Se o limite existe para proteger um recurso com capacidade instantânea limitada, o dobro do teto é exatamente o que se queria evitar.
Token bucket resolve o pico e ainda expressa uma intenção que os outros algoritmos não expressam: taxa média sustentada com rajada tolerada. Um balde de 100 tokens com reposição de 10 por segundo permite que o cliente acumule folga durante um período ocioso e gaste tudo de uma vez — comportamento típico de integração que roda em lote de madrugada, e que faz sentido permitir.
A recomendação prática: token bucket para o limite por tenant e por credencial, onde rajada é legítima; janela deslizante para rotas caras, onde rajada é o próprio problema; janela fixa apenas em contadores de billing, onde a simplicidade compensa.
A resposta do 429 é parte da API, não um detalhe de erro
Um limite estourado devolve 429 Too Many Requests. O que acompanha esse status determina se o cliente consegue se comportar bem ou se vai entrar em laço de retentativa.
Três cabeçalhos resolvem quase tudo. RateLimit-Limit informa o teto da janela. RateLimit-Remaining informa o saldo. RateLimit-Reset informa em quantos segundos o saldo volta. Em respostas de bloqueio, Retry-After é o campo que bibliotecas de cliente já sabem interpretar sem código adicional.
Devolver esses cabeçalhos em todas as respostas — não só nas bloqueadas — permite que o cliente desacelere antes de bater no teto. É a diferença entre uma integração que se ajusta sozinha e uma que descobre o limite por colisão.
Falta o corpo. Um 429 com corpo vazio obriga o desenvolvedor do outro lado a adivinhar qual dos limites foi atingido. Um corpo que diga qual chave estourou, qual era o teto e quando ele volta economiza uma abertura de chamado por cliente novo:
{
"error": "rate_limit_exceeded",
"scope": "tenant",
"limit": 1000,
"window_seconds": 60,
"retry_after_seconds": 23
}
O campo scope é o que mais poupa tempo de suporte: com três níveis de limite ativos, saber que o estouro foi na credencial e não no tenant muda completamente a ação do cliente — trocar de integração em vez de comprar mais cota.
Antes de subir: o que costuma faltar
Rate limiting entra em produção com três lacunas recorrentes. A primeira é ausência de observabilidade: sem métrica de quantas requisições foram bloqueadas por tenant e por escopo, não há como distinguir um limite bem calibrado de um limite que está silenciosamente quebrando a integração de um cliente grande.
A segunda é ausência de modo de observação. Ligar limites novos direto em bloqueio é apostar que os números foram bem escolhidos. Rodar por duas semanas apenas contando e emitindo métrica, sem bloquear, mostra o percentil real de consumo e evita descobrir o erro pelo canal de suporte.
A terceira é ausência de válvula de escape. Vai existir o cliente com contrato especial, a migração legítima que precisa de dez vezes a cota por três dias, o teste de carga combinado. Se o único jeito de liberar isso for deploy, alguém vai desligar o rate limiting inteiro numa madrugada. Um override por tenant com validade — armazenado junto com os limites, consultado no mesmo lugar — resolve o caso e mantém o rastro de quem liberou o quê e até quando.
O próximo passo concreto: levantar, no tráfego atual, o percentil 99 de requisições por minuto por tenant e por rota. Sem esse número, qualquer limite escolhido é chute — e chute em rate limiting sempre erra para o lado de quem paga mais.
Deixe um comentário