Destino Dev

Blog

Rate limiting por usuário no NestJS com Guard + Redis (sliding window)

Por John Pires · 19 de março de 2026

Rate limiting por usuário no NestJS com Guard + Redis (sliding window)

Por que aprender isso em 2026

Contexto do problema

Rate limiting é a técnica de controlar a quantidade de requisições que um cliente pode fazer a um serviço em um intervalo de tempo. Em 2026, aplicações web e APIs continuam crescendo em escala e distribuição: sistemas serverless, microservices e clientes móveis competem por recursos. Ataques simples de negação de serviço, uso indevido de APIs públicas e picos inesperados de tráfego seguem sendo problemas reais. Controlar o consumo por usuário evita abuso, garante experiência e reduz custos.

Por que usar sliding window e Redis

Existem várias estratégias de rate limiting: fixed window, token bucket, leaky bucket e sliding window. O fixed window é simples, porém gera burst indesejado no começo de cada window. Sliding window suaviza esse efeito, contabilizando eventos por timestamps e permitindo limites mais estáveis. Redis é a escolha natural para aplicar políticas distribuídas: é rápido, tem estruturas (sorted sets) úteis e suporta scripts atômicos (Lua), essenciais para consistência entre múltiplas instâncias do serviço.

Por que implementar no NestJS com Guard

NestJS é um framework Node.js baseado em TypeScript que organiza código com controllers, providers e guards. Guards são ideais para rate limiting porque executam antes do handler: permitem recusar a requisição cedo. Implementar a lógica no Guard mantém controllers limpos e permite aplicar regras por rota, por controlador ou globalmente. Integrar Guard + Redis dá solução escalável e reutilizável.

Use sliding window quando precisar de limites por usuário precisos e comportamentos suaves em picos; combine com Redis e scripts para garantir atomicidade e desempenho.

Configuração do ambiente

Requisitos mínimos

Para seguir os exemplos você precisa de:

  • Node.js 18+
  • npm ou yarn
  • NestJS CLI (opcional, mas recomendado)
  • Redis (local via Docker ou remoto)

Instale dependências essenciais no projeto Nest:

npm init -y
npm i @nestjs/core @nestjs/common @nestjs/platform-express reflect-metadata rxjs
npm i ioredis
npm i --save-dev typescript ts-node-dev @types/node

O código acima cria um projeto básico e instala o ioredis, cliente Redis recomendado.

Rodando Redis localmente

Se você tem Docker:

docker run -d --name redis -p 6379:6379 redis:7-alpine

Isso expõe Redis na porta padrão 6379. Em produção use provisionamento adequado e autenticação.

Estrutura mínima do projeto

Crie uma estrutura simples:

  1. src/main.ts
  2. src/app.module.ts
  3. src/redis/redis.module.ts
  4. src/guards/rate-limit.guard.ts
  5. src/controllers/sample.controller.ts

O módulo Redis encapsula o cliente, o Guard aplica a lógica e o controller é o ponto de teste das regras.

Hello World progressivo

1) App básico NestJS

Primeiro, um main mínimo para levantar o servidor:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Esse arquivo inicia a aplicação Nest. Depois de rodar, você terá uma HTTP API vazia escutando em 3000.

2) Módulo Redis (provider)

Vamos encapsular o cliente Redis com ioredis para injetar em Guards ou outros serviços.

// src/redis/redis.module.ts
import { Global, Module } from '@nestjs/common';
import Redis from 'ioredis';

const redisProvider = {
  provide: 'REDIS',
  useFactory: () => {
    return new Redis({ host: '127.0.0.1', port: 6379 });
  },
};

@Global()
@Module({ providers: [redisProvider], exports: [redisProvider] })
export class RedisModule {}

Isso cria um provider global chamado 'REDIS'. Injetando 'REDIS' em qualquer lugar você terá o cliente ioredis pronto.

3) Controller de teste

Um controller simples para testar comportamento antes de aplicar o guard:

// src/controllers/sample.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('sample')
export class SampleController {
  @Get()
  get() {
    return { ok: true, time: Date.now() };
  }
}

// Depois adicione SampleController no AppModule

Chamar GET /sample retorna um JSON simples. Próximo passo: aplicar rate limit.

Mini-projeto prático

Estratégia: sliding window com sorted sets

Implementaremos sliding window por usuário usando sorted sets (zset). Ideia:

  1. Para cada requisição, insere-se um score igual ao timestamp em milissegundos no zset com key user:{id}:rl
  2. Remove entradas mais antigas que a janela (timestamp < now - window)
  3. Conta quantos elementos permanecem; se maior que limite, bloqueia
  4. Define um TTL curto na chave para limpeza automática

Essa sequência precisa ser atômica; usaremos um script Lua executado por EVAL para garantir atomicidade entre múltiplas instâncias.

Guard: implementação com Lua para atomicidade

O Guard verifica se a requisição deve ser permitida. Exemplo de Guard com chamada ao Redis e uso de script Lua:

// src/guards/rate-limit.guard.ts
import { CanActivate, ExecutionContext, Injectable, Inject, ForbiddenException } from '@nestjs/common';
import { Request } from 'express';
import Redis from 'ioredis';

@Injectable()
export class RateLimitGuard implements CanActivate {
  constructor(@Inject('REDIS') private readonly redis: Redis) {}

  // Limite: 5 requisições por 10 segundos (exemplo para iniciantes)
  private readonly limit = 5;
  private readonly windowMs = 10_000;

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req: Request = context.switchToHttp().getRequest();
    // Identifique o usuário: se não autenticado, use IP
    const userId = (req.user && (req.user as any).id) || req.ip;
    const key = `rl:${userId}`;
    const now = Date.now();

    // Script Lua: insere, remove antigos e conta
    const lua = `
      local key = KEYS[1]
      local now = tonumber(ARGV[1])
      local window = tonumber(ARGV[2])
      local limit = tonumber(ARGV[3])
      local member = ARGV[4]

      redis.call('ZADD', key, now, member)
      redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
      local count = redis.call('ZCARD', key)
      redis.call('PEXPIRE', key, window)
      return count
    `;

    const member = `${now}-${Math.random().toString(36).slice(2)}`;
    const count = await this.redis.eval(lua, 1, key, now, this.windowMs, this.limit, member);

    if (count > this.limit) {
      throw new ForbiddenException('Rate limit exceeded');
    }

    return true;
  }
}

O script Lua realiza os passos atômicos: ZADD, ZREMRANGEBYSCORE, ZCARD e PEXPIRE. Isso previne condições de corrida entre instâncias concorrentes.

Como usar o Guard no controller

Exemplo de aplicação do Guard em uma rota específica:

// src/controllers/sample.controller.ts (com guard)
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RateLimitGuard } from '../guards/rate-limit.guard';

@Controller('sample')
export class SampleController {
  @Get()
  @UseGuards(RateLimitGuard)
  get() {
    return { ok: true, time: Date.now() };
  }
}

// Lembre-se de exportar/registrar RateLimitGuard no AppModule como provider

Agora, cada chamada a GET /sample passa pelo guard que aplica sliding window. Se exceder 5 requisições em 10s por usuário/ip, será retornado 403.

Alternativas e melhorias

Possíveis aprimoramentos:

  • Token bucket para permitir bursts controlados
  • Separar limites por rota/role (usuário gratuito x premium)
  • Expor headers X-RateLimit-* para feedback ao cliente
  • Usar Redis cluster ou cliente com suporte a cluster se necessário

Também é possível usar Interceptor para adicionar headers ou balancear comportamento após o handler, mas Guard é preferível para negar antes do processamento.

Script Lua explicado passo a passo

O script Lua utiliza argumentos para ser genérico. Fluxo:

  1. ZADD adiciona um membro com score timestamp
  2. ZREMRANGEBYSCORE remove todos com score menor que now - window
  3. ZCARD conta elementos restantes
  4. PEXPIRE define TTL para limpeza automática

Retornamos o count para o guard decidir se aceita ou rejeita a requisição. A escolha do member como combinação random+timestamp evita colisões idênticas.

Erros comuns, armadilhas e dicas práticas

Erro: usar get/set em vez de script

Muitos iniciantes fazem: MULTI { get count; if count < limit set count+1; exec }. Sem script, entre get e set outra instância pode inserir, causando inconsistência. Use scripts Lua ou comandos Redis atômicos sempre que múltiplas operações precisam ser atômicas.

Armadilha: relógio do servidor

O sliding window usa timestamps do servidor que executa o Guard. Em ambientes distribuídos, clocks desalinhados podem produzir comportamentos estranhos. Soluções:

  • Use Date.now() no servidor da aplicação; garanta NTP sincronizado em todas as instâncias
  • Ou use REdis time (TIME) como fonte de verdade, mas isso adiciona uma chamada extra

Dica: feedback para o cliente

Adicionar headers ajuda o cliente a respeitar limites: X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset. Use um interceptor para injetar esses headers baseados no resultado do script.

Erro: escolher window/limit inadequados

Parâmetros ruins causam falsos positivos. Teste com tráfego real e métricas. Diferencie limites para endpoints sensíveis e públicos.

Recursos para continuar estudando

Documentação e leituras técnicas

Referências essenciais:

  • Redis docs: sorted sets, Lua scripting, EXPIRE
  • NestJS docs: Guards, Modules, Interceptors
  • Artigos sobre algoritmos de ratelimit: token bucket vs leaky vs sliding window

Ler a documentação oficial do Redis sobre scripts ajuda a entender limites de performance e segurança.

Exercícios práticos

Exercícios recomendados para fixar:

  1. Adicionar headers com interceptor indicando restante e reset
  2. Implementar limites diferentes por rota usando decorators
  3. Substituir member simples por hash de user + requestId para auditoria

Esses exercícios transformam compreensão teórica em código reutilizável e testável.

Conclusão prática

Resumo das decisões

Implementar rate limiting por usuário em NestJS com Guard + Redis usando sliding window oferece balanceamento entre precisão e suavidade no controle de tráfego. O uso de Lua scripts garante atomicidade necessária em ambientes distribuídos. Guards permitem negar requisições cedo, preservando recursos.

Checklist de implantação

Antes de rodar em produção, verifique:

  • Sincronização de relógio entre instâncias
  • Configuração adequada do Redis (replicação, autenticação)
  • Métricas e logs para monitorar rejeições
  • Testes de carga para validar parâmetros

Pronto: você tem um Guard funcional com Redis que aplica sliding window por usuário. O próximo passo é adaptar limites por rota, adicionar headers de feedback e integrar com métricas para observabilidade.

Observação final: para projetos que exigem controle extremamente fino ou garantias adicionais (por exemplo, billing preciso), complemente essa abordagem com armazenamento de eventos ou contadores em banco persistente e revisões arquiteturais específicas.

Assuntos: NestJS, guards, NoSQL, rate-limiting, sliding-window

Leia também