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