Rate limiting controla quantas requisições um cliente pode realizar em um intervalo. Ele protege APIs Node.js contra abuso, automações defeituosas, brute force, scraping agressivo e picos que poderiam esgotar banco, memória ou serviços externos.
O limite não substitui autenticação, WAF, filas, cache ou dimensionamento. Ele é uma camada de proteção e justiça de uso. Uma política mal configurada pode bloquear usuários legítimos, ser contornada por múltiplos IPs ou falhar em ambientes distribuídos.
O que limitar
Escolha a chave de acordo com o risco:
- IP para endpoints públicos;
- usuário autenticado;
- API key;
- tenant ou organização;
- rota ou operação;
- recurso específico;
- combinação de identidade e IP.
Evite usar apenas IP em aplicações atrás de NAT, pois milhares de usuários podem compartilhar o mesmo endereço.
Resposta HTTP 429
res.status(429)
.set('Retry-After', '60')
.json({
error: 'rate_limit_exceeded',
message: 'Muitas requisições. Tente novamente em 60 segundos.',
});O status 429 indica excesso de requisições. Retry-After pode informar segundos ou uma data HTTP. Clientes devem aplicar backoff e não repetir imediatamente.
Fixed window
A janela fixa conta requisições por período, como 100 por minuto. É simples, mas permite rajadas na fronteira: 100 requisições no fim de um minuto e outras 100 no início do seguinte.
const buckets = new Map();
function fixedWindow(key, limit, windowMs) {
const now = Date.now();
const bucketStart = Math.floor(now / windowMs) * windowMs;
const bucketKey = `${key}:${bucketStart}`;
const count = (buckets.get(bucketKey) || 0) + 1;
buckets.set(bucketKey, count);
return { allowed: count <= limit, remaining: Math.max(0, limit - count) };
}Esse exemplo é didático e não faz limpeza, persistência nem coordenação entre processos.
Sliding window log
Armazena timestamps das requisições dentro da janela. É preciso, mas consome memória proporcional ao volume. Em Redis, sorted sets podem implementar o algoritmo, porém exigem operações atômicas e expiração.
Sliding window counter
Aproxima uma janela deslizante combinando contadores da janela atual e anterior. Reduz memória e suaviza fronteiras. É adequada para muitos cenários de API.
Token bucket
Tokens são adicionados a uma taxa até uma capacidade máxima. Cada requisição consome um token. O algoritmo permite rajadas controladas e mantém uma taxa média.
class TokenBucket {
constructor({ capacity, refillPerSecond }) {
this.capacity = capacity;
this.tokens = capacity;
this.refillPerSecond = refillPerSecond;
this.updatedAt = performance.now();
}
consume(cost = 1) {
const now = performance.now();
const elapsedSeconds = (now - this.updatedAt) / 1000;
this.tokens = Math.min(
this.capacity,
this.tokens + elapsedSeconds * this.refillPerSecond,
);
this.updatedAt = now;
if (this.tokens < cost) return false;
this.tokens -= cost;
return true;
}
}Em múltiplas instâncias, o estado precisa ser compartilhado e atualizado atomicamente.
Leaky bucket
O leaky bucket processa itens em uma taxa constante e pode enfileirar uma rajada curta. Ele é útil quando você quer suavizar chamadas a uma dependência, mas precisa definir fila máxima e política de descarte.
Middleware simples
function rateLimit({ limit, windowMs, keyGenerator }) {
return (req, res, next) => {
const key = keyGenerator(req);
const result = limiter.check(key, limit, windowMs);
res.set('RateLimit-Limit', String(limit));
res.set('RateLimit-Remaining', String(result.remaining));
if (!result.allowed) {
res.set('Retry-After', String(result.retryAfterSeconds));
res.status(429).json({ error: 'rate_limit_exceeded' });
return;
}
next();
};
}Use uma biblioteca madura ou gateway quando possível. Implementações próprias precisam lidar com concorrência, relógio, memória e distribuição.
IP atrás de proxy
X-Forwarded-For pode ser falsificado se a aplicação confia em qualquer origem. Configure a quantidade ou rede de proxies confiáveis no framework.
app.set('trust proxy', 1);O valor correto depende da topologia. Configurar true sem entender a cadeia pode permitir que o cliente escolha o IP usado no limite.
IPv6
Usuários podem possuir muitos endereços IPv6. Limitar pelo endereço completo pode ser contornável; agrupar por prefixo reduz evasão, mas pode afetar redes compartilhadas. Use bibliotecas que tratem IPv4 e IPv6 corretamente.
Usuários autenticados
function keyGenerator(req) {
if (req.user) return `user:${req.user.id}`;
return `ip:${req.ip}`;
}Não use email ou dado pessoal bruto como chave. IDs internos ou hashes controlados são mais adequados.
Limites por plano
const limits = {
free: { requests: 100, windowMs: 60_000 },
pro: { requests: 1000, windowMs: 60_000 },
};
const policy = limits[req.user.plan] || limits.free;Não aceite o plano vindo de header do cliente. Carregue da identidade autenticada ou token assinado.
Custos diferentes
Uma busca simples e uma exportação pesada não deveriam consumir o mesmo. Token bucket pode cobrar custos:
const cost = req.path === '/exports' ? 20 : 1;
const allowed = bucket.consume(cost);Documente a unidade e mantenha política compreensível.
Login e brute force
Para autenticação, combine limites por IP e por conta:
- tentativas por IP;
- tentativas por identificador normalizado;
- atraso progressivo;
- MFA;
- detecção de credenciais vazadas;
- bloqueio temporário com cuidado.
Não revele se uma conta existe. Limites muito rígidos por usuário permitem ataque de negação de serviço contra uma vítima.
Redis
Em múltiplas réplicas, Redis é uma opção comum. A atualização deve ser atômica. Um fixed window pode usar INCR e EXPIRE, mas a expiração precisa ser aplicada sem condição de corrida, normalmente por script Lua ou comando transacional.
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[1])
end
return currentDefina TTL em todas as chaves para evitar crescimento.
Falha do armazenamento
Decida a política:
- fail open: permite requisições se Redis falhar;
- fail closed: bloqueia tudo;
- fallback local: usa limite aproximado por instância.
Para login ou operação financeira, fail closed pode ser mais seguro. Para leitura pública, fail open pode preservar disponibilidade. A decisão é de risco, não apenas técnica.
Gateway ou aplicação
Limitar no CDN, WAF ou API gateway bloqueia tráfego antes de consumir recursos da aplicação. Limitar no Node.js permite usar identidade e regras de negócio. Muitas arquiteturas usam ambos:
- limite amplo por IP na borda;
- limite por usuário e operação na aplicação;
- quota de longo prazo em serviço central.
Rate limit e concorrência
Contar requisições por minuto não impede 100 operações simultâneas. Adicione limite de concorrência:
if (activeByUser.get(userId) >= 5) {
res.status(429).json({ error: 'concurrency_limit_exceeded' });
return;
}
Remova o contador em finish e close. Em ambiente distribuído, coordenação é mais complexa.
Filas
Para tarefas demoradas, aceite a solicitação, crie um job e retorne 202. Limite quantos jobs o usuário pode enfileirar e o tamanho total pendente.
Headers
Além de Retry-After, existem convenções de headers de limite. Padronize e documente. Não prometa precisão absoluta se o algoritmo distribuído é aproximado.
Jitter e clientes
Clientes devem usar exponential backoff com jitter:
const delay = Math.min(maxDelay, baseDelay * 2 ** attempt);
const jittered = Math.random() * delay;
await setTimeout(jittered);Retry automático deve respeitar idempotência e Retry-After.
Cache
Cachear respostas reduz custo, mas não elimina abuso. Uma rota cacheada ainda pode consumir banda, conexões e CDN. Aplique limites adequados à borda.
WebSockets
Limite conexões abertas, mensagens por segundo, tamanho de mensagem e operações caras. O handshake HTTP é apenas uma parte do risco.
GraphQL
Uma requisição pode conter consulta muito cara. Combine rate limit com análise de profundidade, complexidade e custo. Limitar somente o número de requests é insuficiente.
Uploads
Limite tamanho, taxa, quantidade simultânea e volume por período. Rejeite cedo com Content-Length quando confiável, mas também conte bytes realmente recebidos.
Observabilidade
Meça:
- requisições permitidas e bloqueadas;
- chaves ativas agregadas;
- rota e política;
- latência do armazenamento;
- erros do limiter;
- fail open e fallback;
- taxa de 429;
- impacto em conversão e suporte.
Não use user ID ou IP como label de Prometheus. Logs amostrados podem guardar hash controlado para investigação.
Alertas
Um aumento de 429 pode indicar ataque, cliente com bug, limite baixo ou campanha legítima. Correlacione com tráfego, erros, usuários únicos e uso de recursos.
Testes
Teste:
- primeira e última requisição;
- fronteira da janela;
- concorrência;
- múltiplas instâncias;
- falha do Redis;
- TTL;
- IPv6;
- proxy;
- Retry-After;
- recuperação após bloqueio.
Teste de carga
Gere carga com diferentes identidades e rajadas. Confirme que o limiter não vira gargalo central. Meça p99, CPU, memória e latência do Redis.
Erros comuns
- confiar em X-Forwarded-For sem configuração;
- usar mapa local em cluster;
- não expirar chaves;
- bloquear usuários de NAT;
- ignorar IPv6;
- não definir política quando Redis falha;
- limitar requests e ignorar custo;
- usar dados pessoais na chave;
- não retornar Retry-After;
- não monitorar falsos positivos.
Fluxo recomendado
Defina ameaça e identidade, escolha algoritmo, implemente limite na borda e na aplicação, use armazenamento atômico, documente headers e monitore impacto. Combine com Health Checks, Graceful Shutdown, testes com Autocannon e métricas em Prometheus.
Consulte a definição do status HTTP 429 e a documentação oficial do Redis INCR.



