Voltar para Artigos
Back-end9 min de leitura

Rate Limiting em APIs Node.js: Algoritmos, Redis e Estratégias por Rota

Fixed Window vs Sliding Window vs Token Bucket. Implementação com rate-limiter-flexible no Redis (compartilhado entre múltiplas instâncias). Limites diferentes por rota (login mais restrito), bloqueio progressivo por IP e headers de resposta corretos.

12 de agosto de 2026

Uma API sem Rate Limiting é um convite para ataques de força bruta no endpoint de login, enumeração de usuários via reset de senha, e DoS via requisições excessivas. Mais importante: em ambientes com múltiplas instâncias do Node.js (PM2 cluster ou múltiplos containers), o rate limiting em memória local é inútil — cada instância conta separado, e o atacante distribui requisições entre elas.

A solução é centralizar a contagem no Redis: compartilhado entre todas as instâncias, atômico (sem race condition) e extremamente rápido. A biblioteca rate-limiter-flexible implementa vários algoritmos de forma eficiente.

Os Três Algoritmos Principais

  • Fixed Window — conta requisições em janelas fixas (ex: 100 req/minuto a partir de 09:00). Problema: permite burst no final/início de janelas — 100 req às 09:59 + 100 req às 10:00 = 200 req em 2 segundos.
  • Sliding Window — janela que desliza com o tempo. Mais preciso, sem o problema de burst. Custo de memória ligeiramente maior.
  • Token Bucket — cada requisição consome um token. Tokens são reabastecidos a uma taxa constante. Permite bursts controlados — ideal para APIs que precisam de flexibilidade mas com limite de throughput.
bash
npm install rate-limiter-flexible ioredis

Configuração Base com Redis

O ponto crítico é definir limites diferentes por rota. Um endpoint de listagem de produtos pode ter 200 req/min sem problema. Um endpoint de login não deveria ultrapassar 5 tentativas em 15 minutos por IP — qualquer número maior já é sinal de ataque de força bruta. O blockDuration agrava o bloqueio progressivamente: ao exceder o limite de login, o IP fica bloqueado por 30 minutos, desincentivando tentativas futuras.

RateLimiters.ts
import { RateLimiterRedis } from 'rate-limiter-flexible';
import { redisClient } from '../redis/RedisClient';

// Rate Limiter global: protege toda a API
export const globalRateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'rl:global',
  points: 100,          // 100 requisições
  duration: 60,         // por minuto
  blockDuration: 60,    // Bloqueia por 1min ao exceder (retorna 429)
});

// Rate Limiter de login: proteção contra brute force
// Mais restritivo — atacantes testam senhas no endpoint de login
export const loginRateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'rl:login',
  points: 5,            // 5 tentativas
  duration: 15 * 60,    // em 15 minutos
  blockDuration: 30 * 60, // Bloqueia por 30min ao exceder
});

// Rate Limiter de reset de senha: mitigação de enumeração de usuários
export const passwordResetRateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'rl:pwd-reset',
  points: 3,
  duration: 60 * 60,    // 3 tentativas por hora
  blockDuration: 60 * 60,
});

// Rate Limiter de API pública: para rotas sem autenticação
export const publicApiRateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'rl:public',
  points: 30,
  duration: 60,
  blockDuration: 5 * 60,
});

Middleware com Headers de Resposta Corretos

Os headers X-RateLimit-* são um padrão de facto que os clientes da API usam para saber quantas requisições ainda têm disponíveis e quando o limite reseta. Sem esses headers, um cliente bem-comportado não tem como saber se deve fazer throttling próprio. Ao retornar 429, o header Retry-After instrui o cliente sobre quando pode tentar novamente — evitando que ele reenvie imediatamente e esgote o limite mais rápido.

O padrão fail-open no bloco catch é uma decisão deliberada de resiliência: se o Redis estiver temporariamente indisponível, a requisição passa mesmo sem verificação de rate limit. A alternativa (fail-closed) bloquearia todos os usuários legítimos enquanto o Redis estiver fora. Para APIs críticas, o fail-open é preferível — o risco de alguns abusos durante uma janela curta de indisponibilidade do Redis é menor que bloquear todos os usuários legítimos.

rateLimitMiddleware.ts
import type { Request, Response, NextFunction } from 'express';
import type { RateLimiterRedis } from 'rate-limiter-flexible';

// Factory: retorna um middleware para o limiter específico
export function createRateLimitMiddleware(
  limiter: RateLimiterRedis,
  // Chave para identificar a entidade: IP, userId ou email
  keyExtractor: (req: Request) => string = (req) => req.ip as string
) {
  return async function rateLimitMiddleware(
    req: Request,
    res: Response,
    next: NextFunction
  ): Promise<void> {
    try {
      const key = keyExtractor(req);
      const result = await limiter.consume(key);

      // Headers padrão de Rate Limiting (RFC 6585)
      res.setHeader('X-RateLimit-Limit', limiter.points);
      res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
      res.setHeader(
        'X-RateLimit-Reset',
        new Date(Date.now() + result.msBeforeNext).toISOString()
      );

      next();
    } catch (err: unknown) {
      // RateLimiterRes: erro ao exceder o limite
      if (err && typeof err === 'object' && 'msBeforeNext' in err) {
        const retryAfterSecs = Math.ceil((err as { msBeforeNext: number }).msBeforeNext / 1000);

        res.setHeader('Retry-After', retryAfterSecs);
        res.setHeader('X-RateLimit-Remaining', 0);
        res.status(429).json({
          error: 'Muitas requisições. Tente novamente em alguns instantes.',
          retryAfterSeconds: retryAfterSecs,
        });
        return;
      }

      // Erro desconhecido — deixa passar (fail open) para não bloquear
      // usuários legítimos quando o Redis estiver fora
      next();
    }
  };
}

Aplicação nas Rotas

A dupla proteção por IP e por email no endpoint de login é uma técnica importante: limitar apenas por IP é facilmente contornável com IPs rotativos, e limitar apenas por email pode causar bloqueio legítimo do usuário por um atacante que tente propositalmente o login do alvo. Com ambos os limitadores aplicados em sequência, o atacante precisa contornar os dois — o que é muito mais difícil na prática.

sessions.routes.ts
import { Router } from 'express';
import { createRateLimitMiddleware } from '../../rateLimiting/rateLimitMiddleware';
import { loginRateLimiter, passwordResetRateLimiter } from '../../rateLimiting/RateLimiters';

const sessionsRouter = Router();

// Login: rate limit por IP, mas também por email (evita distribuir por IP)
const loginByIp = createRateLimitMiddleware(loginRateLimiter);
const loginByEmail = createRateLimitMiddleware(
  loginRateLimiter,
  (req) => `email:${String(req.body.email).toLowerCase()}` // Chave por email
);

sessionsRouter.post(
  '/sessions',
  loginByIp,    // Limita por IP
  loginByEmail, // E por email (dupla proteção)
  sessionsController.create
);

// Reset de senha: por IP
sessionsRouter.post(
  '/forgot-password',
  createRateLimitMiddleware(passwordResetRateLimiter),
  forgotPasswordController.execute
);

export { sessionsRouter };

Bloqueio por IP em proxies e load balancers: quando a API está atrás de um Nginx ou CloudFront, req.ip pode retornar o IP do proxy, não do usuário. Configure o Express com app.set('trust proxy', 1) e certifique-se que o proxy envia o header X-Forwarded-For correto. Sem isso, todos os usuários compartilham o mesmo IP (o do proxy) e o rate limiting não funciona.

Conclusão

Rate Limiting com Redis é obrigatório para qualquer API em produção. O limite global protege contra DoS. O limite por rota sensível (login, reset) protege contra brute force e enumeração. A chave composta (IP + email) elimina o bypass por IP distribuído. E o fail-open no Redis offline garante que uma indisponibilidade do Redis não derrube toda a API.