Rate limiting com Redis no Node.js
Rate limiting protege sua API de abuso, bots e picos de tráfego que derrubam o servidor. Com Redis como backend distribuído, o limite funciona corretamente mesmo com múltiplas instâncias da API em produção — algo impossível com rate limiting em memória local.
Rate limiting com express-rate-limit + Redis
Setup rápido com a biblioteca padrão do Express:
typescript
// npm install express-rate-limit rate-limit-redis ioredis
import rateLimit from 'express-rate-limit'
import { RedisStore } from 'rate-limit-redis'
import Redis from 'ioredis'
const redis = new Redis({ host: 'localhost', port: 6379 })
// Rate limiter geral — 100 req/15min por IP:
const limiterGeral = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100,
standardHeaders: 'draft-7', // retorna RateLimit headers no padrão IETF
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: 'rl:geral:',
}),
handler: (req, res) => {
res.status(429).json({
error: 'Muitas requisições. Tente novamente em alguns minutos.',
retryAfter: res.getHeader('Retry-After'),
})
},
})
// Rate limiter estrito para auth — 5 tentativas/hora por IP:
const limiterAuth = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora
max: 5,
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: 'rl:auth:',
}),
})
app.use('/api/', limiterGeral)
app.use('/api/auth/', limiterAuth)Sliding window com Lua script no Redis
Algoritmo preciso de janela deslizante, atômico e eficiente:
typescript
// Sliding window com sorted set — mais preciso que fixed window
import Redis from 'ioredis'
const redis = new Redis()
const SLIDING_WINDOW_SCRIPT = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
-- Remover entradas fora da janela:
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
-- Contar entradas na janela atual:
local count = redis.call('ZCARD', key)
if count < limit then
-- Adicionar esta requisição:
redis.call('ZADD', key, now, now .. math.random())
redis.call('EXPIRE', key, math.ceil(window / 1000))
return { 1, limit - count - 1 } -- allowed, remaining
else
return { 0, 0 } -- denied
end
`
async function slidingWindowLimit(
identificador: string,
limite: number,
janelaMs: number
): Promise<{ permitido: boolean; restante: number }> {
const chave = `sw:${identificador}`
const [permitido, restante] = await redis.eval(
SLIDING_WINDOW_SCRIPT, 1, chave,
Date.now(), janelaMs, limite
) as [number, number]
return { permitido: permitido === 1, restante }
}Rate limiting por usuário autenticado
Limites diferentes para planos Free, Pro e Enterprise:
typescript
// Limites por plano com Redis:
const LIMITES_POR_PLANO = {
free: { max: 100, windowMs: 60 * 60 * 1000 }, // 100/hora
pro: { max: 5000, windowMs: 60 * 60 * 1000 }, // 5k/hora
enterprise: { max: 50000, windowMs: 60 * 60 * 1000 }, // 50k/hora
}
async function rateLimitMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
// Usar JWT do usuário como identificador (não IP):
const userId = req.user?.id
if (!userId) return next() // não autenticado: limiter de IP já aplicado
const plano = req.user?.plano ?? 'free'
const { max, windowMs } = LIMITES_POR_PLANO[plano]
const chave = `rl:user:${userId}`
const agora = Date.now()
const [contagem] = await redis
.multi()
.incr(chave)
.pexpire(chave, windowMs)
.exec() as [[null, number], [null, number]]
const restante = Math.max(0, max - contagem)
res.setHeader('X-RateLimit-Limit', max)
res.setHeader('X-RateLimit-Remaining', restante)
res.setHeader('X-RateLimit-Reset', Math.ceil((agora + windowMs) / 1000))
if (contagem > max) {
return res.status(429).json({ error: 'Limite de requisições atingido' })
}
next()
}
app.use('/api/', rateLimitMiddleware)Token bucket para rajadas controladas
Permitir rajadas curtas respeitando taxa média:
typescript
// Token bucket: permite rajadas mas limita taxa média
// Ideal para APIs que precisam de burst mas não querem travar tráfego legítimo
async function tokenBucket(
identificador: string,
capacidade: number, // max tokens (tamanho do "balde")
taxaRecarga: number // tokens por segundo
): Promise<boolean> {
const chave = `tb:${identificador}`
const agora = Date.now() / 1000 // em segundos
const script = `
local key = KEYS[1]
local capacidade = tonumber(ARGV[1])
local taxa = tonumber(ARGV[2])
local agora = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'ultimo')
local tokens = tonumber(data[1]) or capacidade
local ultimo = tonumber(data[2]) or agora
-- Recarregar tokens baseado no tempo decorrido:
local recarregados = (agora - ultimo) * taxa
tokens = math.min(capacidade, tokens + recarregados)
if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'ultimo', agora)
redis.call('EXPIRE', key, math.ceil(capacidade / taxa) + 1)
return 1
else
redis.call('HSET', key, 'ultimo', agora)
return 0
end
`
const resultado = await redis.eval(
script, 1, chave, capacidade, taxaRecarga, agora
) as number
return resultado === 1
}
// Uso: 10 req/s com burst de até 50:
app.use(async (req, res, next) => {
if (await tokenBucket(req.ip!, 50, 10)) return next()
res.status(429).json({ error: 'Rate limit exceeded' })
})Monitorar e ajustar rate limits
Observar padrões de uso e calibrar os limites:
bash
# Ver chaves de rate limiting ativas no Redis:
redis-cli --scan --pattern "rl:*" | head -20
# Quantas chaves de rate limit existem:
redis-cli --scan --pattern "rl:*" | wc -l
# Ver contagem atual de um usuário específico:
redis-cli get "rl:user:USER_ID_AQUI"
# Resetar rate limit de um usuário manualmente:
redis-cli del "rl:user:USER_ID_AQUI"
# Monitorar 429s no Nginx:
sudo grep " 429 " /var/log/nginx/access.log | wc -l
# IPs com mais 429s (possíveis abusos):
sudo grep " 429 " /var/log/nginx/access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -10
# Alertar quando 429s estão altos (> 1% do tráfego):
TOTAL=$(sudo grep "$(date '+%d/%b/%Y')" /var/log/nginx/access.log | wc -l)
BLOQUEADOS=$(sudo grep "$(date '+%d/%b/%Y')" /var/log/nginx/access.log | grep " 429 " | wc -l)
PORCENTAGEM=$(echo "scale=2; $BLOQUEADOS * 100 / $TOTAL" | bc)
echo "429s hoje: $BLOQUEADOS / $TOTAL ($PORCENTAGEM%)"$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.