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.

    Perguntas frequentes