Sem rate limiting, qualquer cliente abre a API em milhares de requests por segundo e leva o servidor junto. Com limite mal calibrado, você corta cliente pagante no meio do checkout. Rate limiting certo protege o backend e avisa o cliente antes de recusar.
Os algoritmos e o que cada um mede
Fixed Window
Conta requests dentro de janela fixa, 100 por minuto por exemplo. A fronteira cria o problema do edge burst: 100 requests no segundo 59 da janela mais 100 no segundo 0 da seguinte passam em dois segundos.
Sliding Window Log
Guarda o timestamp de cada request e conta apenas os da janela corrente. Preciso em qualquer posição da fronteira, e o consumo de memória cresce com o volume de requests.
Token Bucket
O bucket enche em taxa fixa e cada request gasta um token. Tokens existentes autorizam o burst e a reposição controla o teto médio por período. O RFC 2697 (Heinanen e Baker, 1999) descreve o marcador de três cores com bucket de tokens, e gateways de API copiaram esse desenho.
// Token bucket com Redis
async function isRateLimited(
key: string,
maxTokens: number,
refillRate: number
): Promise<boolean> {
const now = Date.now()
const bucket = await redis.hgetall(`ratelimit:${key}`)
if (!bucket.tokens) {
// Primeiro request cria o bucket
await redis.hset(`ratelimit:${key}`, {
tokens: maxTokens - 1,
lastRefill: now
})
await redis.expire(`ratelimit:${key}`, 60)
return false
}
// Repõe tokens conforme o tempo decorrido
const elapsed = now - Number(bucket.lastRefill)
const refill = Math.floor(elapsed / 1000 * refillRate)
const tokens = Math.min(maxTokens, Number(bucket.tokens) + refill)
if (tokens <= 0) return true
await redis.hset(`ratelimit:${key}`, {
tokens: tokens - 1,
lastRefill: now
})
return false
}
A documentação do Redis traz uma receita de rate limiter com sorted sets, que resolve a janela deslizante sem varrer logs antigos.
A chave define quem leva o limite
- Por IP: segura bot e abuso anônimo. CGNAT e VPN colocam usuários legítimos no mesmo balde.
- Por usuário autenticado: o limite acompanha o plano que o cliente paga.
- Por API key: bucket por desenvolvedor em API pública. O abuso de um não derruba o outro.
- Composto: teto por IP por cima de teto por usuário. DDoS e abuso de conta atendidos na mesma passada.
Headers que o cliente lê antes de insistir
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 23
X-RateLimit-Reset: 1711234567
Retry-After: 30 // presente no 429
X-RateLimit-* virou costume de mercado pela documentação da API do Twitter e continua sem RFC. O IETF trabalha na padronização no draft-ietf-httpapi-ratelimit-headers (Polli e Martinez, IETF HTTPAPI WG), que define os campos RateLimit e RateLimit-Policy com semântica explícita de janela. O RFC 6585 (Nottingham e Fielding, 2012) criou o status 429 e prevê Retry-After na resposta. Sem esse header, o cliente chuta o tempo de espera e entra em loop de retry contra o seu próprio rate limit.
express-rate-limit com store no Redis
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
}),
keyGenerator: (req) => req.user?.id || req.ip,
handler: (req, res) => {
res.status(429).json({
error: {
code: 'RATE_LIMITED',
message: 'Muitas requisições. Tente novamente em alguns segundos.',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
}
})
}
})
app.use('/api/', limiter)
O keyGenerator devolve o id do usuário quando existe sessão e cai para IP no tráfego anônimo. O store em Redis compartilha o contador entre instâncias; contador em memória local faz cada pod contar por si, e o limite vira max vezes o número de pods. A documentação do express-rate-limit detalha standardHeaders e os avisos de proxy atrás de load balancer.
Limite por custo de rota
Login roda bcrypt e custa milissegundos por tentativa; listagem roda índice e custa microssegundos. Um limite único para as duas pune o fluxo barato.
// Login: 5 tentativas por minuto segura brute force
app.use('/api/auth/login', rateLimit({ windowMs: 60000, max: 5 }))
// Listagens: 100 por minuto
app.use('/api/products', rateLimit({ windowMs: 60000, max: 100 }))
// Upload: 10 por hora
app.use('/api/upload', rateLimit({ windowMs: 3600000, max: 10 }))
Quando o limite cai em cima do cliente
429 com Retry-After é o mínimo do contrato. Em cima disso, graceful degradation devolve versão em cache ou dados parciais quando o dado existe, em vez de erro puro. O capítulo Handling Overload do Google SRE Book (2016) lembra que rejeitar request também consome recursos do servidor e que respostas degradadas mantêm o serviço de pé durante o pico. O cliente recebe algo utilizável, e o backend fica com CPU para o tráfego que passa.
Referências
- RFC 6585, 429 Too Many Requests (Nottingham e Fielding, 2012)
- draft-ietf-httpapi-ratelimit-headers, RateLimit header fields for HTTP (Polli e Martinez, IETF HTTPAPI WG)
- RFC 2697, A Single Rate Three Color Marker (Heinanen e Baker, 1999)
- Site Reliability Engineering, cap. 21, Handling Overload (Google, O'Reilly, 2016)
- Redis docs: rate limiter e express-rate-limit
Curtiu o conteúdo?
Construo produtos web e soluções com IA do jeito certo — arquitetura sólida, código sustentável e entrega real.
Vamos conversar