Por que limitar requisições em memória local é uma ilusão perigosa quando você escala horizontalmente, e como proteger sua infraestrutura com coordenação atômica no Redis.
Em ambientes corporativos, toda API pública ou interna precisa de um mecanismo para conter abusos, garantir justiça no uso de recursos (fairness) e evitar que clientes desgovernados derrubem bancos de dados com rajadas imprevisíveis. A primeira abordagem de muitos desenvolvedores é configurar uma biblioteca de limitação em memória dentro da própria aplicação (como o Rate Limiter nativo do ASP.NET Core).
Em uma máquina de desenvolvimento ou em um servidor isolado, essa solução parece funcionar. O problema começa no momento em que você coloca essa API atrás de um balanceador de carga com 10, 20 ou 50 réplicas em contêineres Kubernetes. De repente, uma regra pensada para permitir 100 requisições por minuto por cliente passa a aceitar até 5.000 requisições por minuto, simplesmente porque o tráfego foi pulverizado entre instâncias que não conversam entre si.
Para construir sistemas verdadeiramente resilientes em escala, o controle de vazão precisa ser distribuído, centralizado e atômico.
O Problema: O Limite Local e o Efeito "Noisy Neighbor"
Quando cada pod da aplicação mantém seu próprio contador em memória, o limite configurado deixa de ser um teto global e se torna um multiplicador proporcional à quantidade de pods:
- Se você tem 20 pods e cada um permite 100 req/min por IP, o atacante pode atingir 2.000 req/min sem receber nenhum código 429.
- Se o Kubernetes faz auto-scaling e sobe mais 10 pods durante um pico de tráfego, a sua capacidade defensiva diminui proporcionalmente, pois a cota total aceita aumenta no momento de maior fragilidade do sistema.
O Cenário do Desastre: O Parceiro Desgovernado
Um sistema parceiro coloca no ar uma rotina de sincronização noturna mal configurada, com centenas de threads paralelas chamando a sua API de busca de produtos. As requisições são distribuídas perfeitamente pelo Ingress Controller entre os seus pods. Como nenhum pod atinge o limite isolado, todas as chamadas descem para o banco de dados relacional. O pool de conexões satura, as threads do banco travam em fila e a API colapsa para todos os outros clientes pagantes (o clássico problema do Noisy Neighbor).
A Solução: Algoritmos de Vazão e Coordenação com Redis
Para implementar rate limiting distribuído, precisamos de duas decisões fundamentais: o algoritmo de controle e o mecanismo de persistência de estado.
1. Fixed Window vs. Sliding Window vs. Token Bucket
- Fixed Window (Janela Fixa): Reinicia a contagem no início de cada minuto. O problema: se um cliente envia 100 requisições às 10:00:59 e mais 100 às 10:01:01, ele injetou 200 requisições em um intervalo de dois segundos, dobrando a carga sem violar a regra.
- Sliding Window Counter (Janela Deslizante): Pondera a contagem da janela anterior com a janela atual com base no tempo decorrido, eliminando picos nas bordas com consumo mínimo de memória.
- Token Bucket (Balde de Fichas): O algoritmo padrão da indústria (usado por AWS e Stripe). O balde tem capacidade máxima (Burst) e é reabastecido continuamente a uma taxa fixa (Tokens por segundo). Permite rajadas rápidas legítimas sem estourar a vazão média.
O Perigo das Race Conditions: Por que GET + SET no Redis Falha
Ao conectar sua API ao Redis, o erro mais comum é fazer a checagem em dois passos via aplicação:
// CÓDIGO PERIGOSO - NUNCA FAÇA ISSO EM PRODUÇÃO
var contagem = await redis.StringGetAsync(chave);
if (contagem < limite)
{
await redis.StringIncrementAsync(chave);
return ProseguirRequisicao();
}
return StatusCode(429);
Sob concorrência real, 50 requisições paralelas leem o mesmo valor simultaneamente antes que qualquer uma consiga incrementar o contador. Todas são aprovadas, furando a cota do cliente. A solução definitiva é executar a lógica diretamente no motor do Redis através de um Script Lua Atômico, garantindo que leitura, incremento e expiração aconteçam em uma única operação indivisível de fração de milissegundo.
Onde isso vive na arquitetura? Implementando com Lua Script em .NET
Veja como implementamos um limitador atômico de janela fixa deslizante utilizando o StackExchange.Redis no ASP.NET Core:
public class DistributedRateLimiter
{
private readonly IDatabase _database;
// Script Lua executado atomicamente pelo Redis
private const string RateLimitScript = @"
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current
";
public DistributedRateLimiter(IConnectionMultiplexer redis)
{
_database = redis.GetDatabase();
}
public async Task<bool> IsRequestAllowedAsync(string clientId, int maxRequests, int windowSeconds)
{
var key = $"ratelimit:{clientId}:{DateTimeOffset.UtcNow.ToUnixTimeSeconds() / windowSeconds}";
var result = (long)await _database.ScriptEvaluateAsync(
RateLimitScript,
keys: new RedisKey[] { key },
values: new RedisValue[] { windowSeconds }
);
return result <= maxRequests;
}
}
As Três Regras de Ouro do Rate Limiting em Produção
- Respeite os Cabeçalhos Padronizados (IETF/RFC): Quando rejeitar uma requisição com status
429 Too Many Requests, sempre informe os cabeçalhosRetry-After(tempo em segundos para tentar novamente),RateLimit-LimiteRateLimit-Remaining. Sem esses cabeçalhos, os clientes entram em loops cegos de retentativa agressiva. - Defina uma Política Clara de Falha (Fail-Open vs. Fail-Closed): O que acontece se o cluster do Redis ficar indisponível? Para a maioria dos produtos de software corporativo, a regra deve ser Fail-Open (permitir a requisição e logar o alerta). Uma indisponibilidade temporária na infraestrutura de cache não deve derrubar o faturamento da empresa inteira.
- Chaves de Particionamento Granulares: Nunca limite exclusivamente por endereço IP. Em redes corporativas ou operadoras móveis, centenas de usuários legítimos compartilham o mesmo IP público via NAT. Prefira compor a chave com
TenantID + ApiKeyouUserID + Rota.
Conclusão
Construir APIs de alto desempenho exige proteger a infraestrutura contra o imprevisto. Limitar taxa de requisições não é punir o cliente; é estabelecer um contrato técnico transparente que garante que nenhum participante desgovernado comprometa a disponibilidade do ecossistema inteiro.
Ao centralizar o estado no Redis com scripts atômicos e adotar algoritmos modernos de controle de fluxo, sua arquitetura ganha previsibilidade, segurança e a capacidade real de escalar horizontalmente sem medo.