Rate Limiting em APIs Node.js: Algoritmos, Redis e Estratégias por Rota
Fixed Window vs Sliding Window vs Token Bucket. Implementação com rate-limiter-flexible no Redis (compartilhado entre múltiplas instâncias). Limites diferentes por rota (login mais restrito), bloqueio progressivo por IP e headers de resposta corretos.
Uma API sem Rate Limiting é um convite para ataques de força bruta no endpoint de login, enumeração de usuários via reset de senha, e DoS via requisições excessivas. Mais importante: em ambientes com múltiplas instâncias do Node.js (PM2 cluster ou múltiplos containers), o rate limiting em memória local é inútil — cada instância conta separado, e o atacante distribui requisições entre elas.
A solução é centralizar a contagem no Redis: compartilhado entre todas as instâncias, atômico (sem race condition) e extremamente rápido. A biblioteca rate-limiter-flexible implementa vários algoritmos de forma eficiente.
Os Três Algoritmos Principais
- Fixed Window — conta requisições em janelas fixas (ex: 100 req/minuto a partir de 09:00). Problema: permite burst no final/início de janelas — 100 req às 09:59 + 100 req às 10:00 = 200 req em 2 segundos.
- Sliding Window — janela que desliza com o tempo. Mais preciso, sem o problema de burst. Custo de memória ligeiramente maior.
- Token Bucket — cada requisição consome um token. Tokens são reabastecidos a uma taxa constante. Permite bursts controlados — ideal para APIs que precisam de flexibilidade mas com limite de throughput.
npm install rate-limiter-flexible ioredisConfiguração Base com Redis
O ponto crítico é definir limites diferentes por rota. Um endpoint de listagem de produtos pode ter 200 req/min sem problema. Um endpoint de login não deveria ultrapassar 5 tentativas em 15 minutos por IP — qualquer número maior já é sinal de ataque de força bruta. O blockDuration agrava o bloqueio progressivamente: ao exceder o limite de login, o IP fica bloqueado por 30 minutos, desincentivando tentativas futuras.
import { RateLimiterRedis } from 'rate-limiter-flexible';
import { redisClient } from '../redis/RedisClient';
// Rate Limiter global: protege toda a API
export const globalRateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:global',
points: 100, // 100 requisições
duration: 60, // por minuto
blockDuration: 60, // Bloqueia por 1min ao exceder (retorna 429)
});
// Rate Limiter de login: proteção contra brute force
// Mais restritivo — atacantes testam senhas no endpoint de login
export const loginRateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:login',
points: 5, // 5 tentativas
duration: 15 * 60, // em 15 minutos
blockDuration: 30 * 60, // Bloqueia por 30min ao exceder
});
// Rate Limiter de reset de senha: mitigação de enumeração de usuários
export const passwordResetRateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:pwd-reset',
points: 3,
duration: 60 * 60, // 3 tentativas por hora
blockDuration: 60 * 60,
});
// Rate Limiter de API pública: para rotas sem autenticação
export const publicApiRateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:public',
points: 30,
duration: 60,
blockDuration: 5 * 60,
});Middleware com Headers de Resposta Corretos
Os headers X-RateLimit-* são um padrão de facto que os clientes da API usam para saber quantas requisições ainda têm disponíveis e quando o limite reseta. Sem esses headers, um cliente bem-comportado não tem como saber se deve fazer throttling próprio. Ao retornar 429, o header Retry-After instrui o cliente sobre quando pode tentar novamente — evitando que ele reenvie imediatamente e esgote o limite mais rápido.
O padrão fail-open no bloco catch é uma decisão deliberada de resiliência: se o Redis estiver temporariamente indisponível, a requisição passa mesmo sem verificação de rate limit. A alternativa (fail-closed) bloquearia todos os usuários legítimos enquanto o Redis estiver fora. Para APIs críticas, o fail-open é preferível — o risco de alguns abusos durante uma janela curta de indisponibilidade do Redis é menor que bloquear todos os usuários legítimos.
import type { Request, Response, NextFunction } from 'express';
import type { RateLimiterRedis } from 'rate-limiter-flexible';
// Factory: retorna um middleware para o limiter específico
export function createRateLimitMiddleware(
limiter: RateLimiterRedis,
// Chave para identificar a entidade: IP, userId ou email
keyExtractor: (req: Request) => string = (req) => req.ip as string
) {
return async function rateLimitMiddleware(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const key = keyExtractor(req);
const result = await limiter.consume(key);
// Headers padrão de Rate Limiting (RFC 6585)
res.setHeader('X-RateLimit-Limit', limiter.points);
res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
res.setHeader(
'X-RateLimit-Reset',
new Date(Date.now() + result.msBeforeNext).toISOString()
);
next();
} catch (err: unknown) {
// RateLimiterRes: erro ao exceder o limite
if (err && typeof err === 'object' && 'msBeforeNext' in err) {
const retryAfterSecs = Math.ceil((err as { msBeforeNext: number }).msBeforeNext / 1000);
res.setHeader('Retry-After', retryAfterSecs);
res.setHeader('X-RateLimit-Remaining', 0);
res.status(429).json({
error: 'Muitas requisições. Tente novamente em alguns instantes.',
retryAfterSeconds: retryAfterSecs,
});
return;
}
// Erro desconhecido — deixa passar (fail open) para não bloquear
// usuários legítimos quando o Redis estiver fora
next();
}
};
}Aplicação nas Rotas
A dupla proteção por IP e por email no endpoint de login é uma técnica importante: limitar apenas por IP é facilmente contornável com IPs rotativos, e limitar apenas por email pode causar bloqueio legítimo do usuário por um atacante que tente propositalmente o login do alvo. Com ambos os limitadores aplicados em sequência, o atacante precisa contornar os dois — o que é muito mais difícil na prática.
import { Router } from 'express';
import { createRateLimitMiddleware } from '../../rateLimiting/rateLimitMiddleware';
import { loginRateLimiter, passwordResetRateLimiter } from '../../rateLimiting/RateLimiters';
const sessionsRouter = Router();
// Login: rate limit por IP, mas também por email (evita distribuir por IP)
const loginByIp = createRateLimitMiddleware(loginRateLimiter);
const loginByEmail = createRateLimitMiddleware(
loginRateLimiter,
(req) => `email:${String(req.body.email).toLowerCase()}` // Chave por email
);
sessionsRouter.post(
'/sessions',
loginByIp, // Limita por IP
loginByEmail, // E por email (dupla proteção)
sessionsController.create
);
// Reset de senha: por IP
sessionsRouter.post(
'/forgot-password',
createRateLimitMiddleware(passwordResetRateLimiter),
forgotPasswordController.execute
);
export { sessionsRouter };Bloqueio por IP em proxies e load balancers: quando a API está atrás de um Nginx ou CloudFront, req.ip pode retornar o IP do proxy, não do usuário. Configure o Express com app.set('trust proxy', 1) e certifique-se que o proxy envia o header X-Forwarded-For correto. Sem isso, todos os usuários compartilham o mesmo IP (o do proxy) e o rate limiting não funciona.
Conclusão
Rate Limiting com Redis é obrigatório para qualquer API em produção. O limite global protege contra DoS. O limite por rota sensível (login, reset) protege contra brute force e enumeração. A chave composta (IP + email) elimina o bypass por IP distribuído. E o fail-open no Redis offline garante que uma indisponibilidade do Redis não derrube toda a API.