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%)"

Não quer configurar manualmente?

Implante o VPS para APIs em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Com PM2 cluster (4 instâncias), cada instância tem seu próprio rate limiter em memória. Um usuário pode fazer 4× o limite enviando requisições distribuídas entre as instâncias. Com Redis como backend compartilhado: todas as instâncias consultam o mesmo contador, o limite é respeitado independente de qual instância responde.

Fixed window: simples mas tem o problema da borda, um usuário pode fazer 2× o limite enviando metade no final de uma janela e metade no início da próxima. Sliding window: mais preciso, sem borda, mas usa mais memória no Redis (sorted set vs counter). Para a maioria das APIs: fixed window com janelas curtas (1 min) é suficiente. Para APIs financeiras ou de segurança: sliding window.

Depende da calibração. Regras: endpoint de login: 5-10 tentativas/hora por IP. API geral: 100-1.000 req/hora dependendo do produto. Webhook receivers: sem limite ou limite muito alto (são eventos, não polling). Monitore os 429s, se usuários legítimos estão sendo bloqueados: aumente o limite ou use rate limiting por usuário autenticado (que pode ter limites mais altos).

Nginx rate limiting (limit_req_zone): mais eficiente, bloqueia antes de chegar na aplicação, mas sem acesso à identidade do usuário. Aplicação com Redis: pode limitar por usuário autenticado, plano de assinatura, endpoint específico. Melhor dos dois: Nginx para limitar IPs agressivos antes de chegar na aplicação, Redis na aplicação para limites granulares por usuário.

Sinais de bot: User-Agent suspeito, padrão de acesso regular demais (exatamente N req/s), sem headers Accept-Language/Accept-Encoding, requisições em horários suspeitos. Para proteção avançada: combine rate limiting com Cloudflare Bot Fight Mode, CrowdSec e honeypot endpoints (URLs que só bots acessam). Rate limiting puro não distingue, apenas limita.