API Rate Limiting: Patterns and Best Practices

Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.

E
ECOSIRE Research and Development Team
|19 de março de 202611 min de leitura2.4k Palavras|

Limitação de taxa de API: padrões e práticas recomendadas

Cada endpoint de API pública é um alvo – bots, scrapers e malfeitores irão atacar seu servidor no momento em que você entrar no ar. Sem limitação de taxa, um único cliente com comportamento inadequado pode esgotar suas conexões de banco de dados, aumentar sua conta de nuvem e interromper o serviço de todos os usuários legítimos. A limitação de taxa não é opcional; é a primeira linha de defesa para qualquer API de produção.

Este guia aborda os quatro principais algoritmos de limitação de taxa, suas vantagens e desvantagens e como implementá-los corretamente no NestJS com Redis. Você sairá com configurações de copiar e colar para cenários comuns e um modelo mental para escolher a estratégia certa por endpoint.

Principais conclusões

  • O bucket de token permite o bursting controlado, enquanto os contadores de janela fixa são os mais baratos de implementar
  • O registro da janela deslizante é o mais preciso, mas consome muita memória; balcão de janela deslizante é o melhor equilíbrio
  • Redis é o único armazenamento de apoio correto quando você executa várias réplicas de API
  • NestJS @nestjs/throttler oferece suporte a adaptadores de armazenamento personalizados — troque no Redis com uma alteração de configuração
  • Sempre retorne os cabeçalhos Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset
  • Diferenciar limites por sensibilidade do endpoint: autenticação (5/min) vs APIs de leitura (1000/min)
  • Use limites baseados em IP para tráfego anônimo e limites baseados em usuários para solicitações autenticadas
  • Nunca descarte solicitações silenciosamente — sempre retorne 429 Too Many Requests com uma mensagem útil

Os quatro algoritmos principais

Contador de janela fixa

A abordagem mais simples: contar solicitações em uma janela de tempo fixa e redefinir no limite.

// Fixed window: 100 requests per minute
// Window resets at :00, :01, :02 ...
const windowKey = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(windowKey);
await redis.expire(windowKey, 60);

if (count > 100) {
  throw new TooManyRequestsException();
}

Fraqueza: A exploração de limites. Um cliente pode enviar 100 solicitações às 12h00min59s e outras 100 às 12h01min00s – efetivamente 200 solicitações em dois segundos. Para a maioria das APIs isso é aceitável. Para endpoints de autenticação, não é.

Registro da janela deslizante

Armazene o carimbo de data/hora de cada solicitação. Em cada solicitação, conte os carimbos de data/hora na última janela.

const now = Date.now();
const windowStart = now - 60_000; // 60 seconds ago

// Remove old entries, add current
await redis.zremrangebyscore(key, 0, windowStart);
await redis.zadd(key, now, now.toString());
const count = await redis.zcard(key);
await redis.expire(key, 60);

if (count > 100) {
  throw new TooManyRequestsException();
}

Compensação: Perfeitamente preciso, mas armazena O(n) entradas por usuário, onde n é a contagem de solicitações. A 1.000 RPS para 10.000 usuários, sua memória Redis aumenta rapidamente. Use para endpoints de baixo volume e alta segurança, como redefinição de senha.

Contador de janela deslizante

Janela deslizante aproximada usando duas janelas fixas — sem explosão de memória.

const now = Date.now();
const currentWindow = Math.floor(now / 60000);
const previousWindow = currentWindow - 1;
const windowProgress = (now % 60000) / 60000; // 0.0 to 1.0

const [current, previous] = await redis.mget(
  `rl:${userId}:${currentWindow}`,
  `rl:${userId}:${previousWindow}`
);

const estimated =
  (parseInt(previous ?? '0') * (1 - windowProgress)) +
  parseInt(current ?? '0');

if (estimated >= 100) {
  throw new TooManyRequestsException();
}

await redis.incr(`rl:${userId}:${currentWindow}`);
await redis.expire(`rl:${userId}:${currentWindow}`, 120);

Este é o algoritmo que Cloudflare usa. Ele suaviza o pico de limite com sobrecarga mínima – duas chaves Redis por usuário por janela.

Balde de tokens

O padrão ouro para permitir rajadas enquanto mantém uma taxa de longo prazo. Cada usuário possui um balde que enche a uma taxa constante. As solicitações consomem tokens.

async function consumeToken(
  redis: Redis,
  userId: string,
  ratePerSec: number,
  capacity: number
): Promise<boolean> {
  const now = Date.now() / 1000;
  const key = `bucket:${userId}`;

  const values = await redis.hmget(key, 'tokens', 'lastRefill');
  const currentTokens = parseFloat(values[0] ?? String(capacity));
  const lastRefillTime = parseFloat(values[1] ?? String(now));

  // Refill tokens based on elapsed time
  const elapsed = now - lastRefillTime;
  const refilled = Math.min(capacity, currentTokens + elapsed * ratePerSec);

  if (refilled < 1) {
    return false; // No tokens available
  }

  await redis.hset(key, 'tokens', String(refilled - 1), 'lastRefill', String(now));
  await redis.expire(key, Math.ceil(capacity / ratePerSec) + 60);

  return true;
}

O bucket de token é ideal para APIs que precisam permitir rajadas curtas (upload de 10 arquivos de uma vez), evitando abusos contínuos.


Configuração do acelerador NestJS

@nestjs/throttler v5 é fornecido com um adaptador de armazenamento Redis. Aqui está uma configuração pronta para produção:

pnpm add @nestjs/throttler @nestjs/throttler-storage-redis ioredis
// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
import { APP_GUARD } from '@nestjs/core';

@Module({
  imports: [
    ThrottlerModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        throttlers: [
          { name: 'short',  ttl: 1000,    limit: 5    }, // 5 req/sec burst
          { name: 'medium', ttl: 60000,   limit: 300  }, // 300 req/min
          { name: 'long',   ttl: 3600000, limit: 5000 }, // 5000 req/hr
        ],
        storage: new ThrottlerStorageRedisService({
          host: config.get('REDIS_HOST'),
          port: config.get('REDIS_PORT'),
        }),
      }),
    }),
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

Substituir limites por controlador ou rota:

@Controller('auth')
export class AuthController {
  // Authentication: very strict — 5 attempts per minute
  @Post('login')
  @Throttle({ medium: { ttl: 60000, limit: 5 } })
  async login(@Body() dto: LoginDto) { /* ... */ }

  // Refresh: moderate — 30 per minute
  @Post('refresh')
  @Throttle({ medium: { ttl: 60000, limit: 30 } })
  async refresh(@Body() dto: RefreshDto) { /* ... */ }

  // Skip throttling on the exchange endpoint (protected by one-time code TTL)
  @Post('exchange')
  @SkipThrottle()
  async exchange(@Body() dto: ExchangeDto) { /* ... */ }
}

Geradores de chaves personalizados

Por padrão, o acelerador NestJS usa o IP do cliente. Na produção por trás do Nginx/Cloudflare, você precisa de X-Real-IP ou CF-Connecting-IP.

// throttler-behind-proxy.guard.ts
import { ThrottlerGuard } from '@nestjs/throttler';
import { Injectable, ExecutionContext } from '@nestjs/common';

@Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
  protected async getTracker(req: Record<string, any>): Promise<string> {
    // Authenticated user — use userId for accurate per-user limits
    if (req.user?.sub) {
      return `user:${req.user.sub}`;
    }
    // Anonymous — use real IP from Cloudflare header
    return (
      req.headers['cf-connecting-ip'] ||
      req.headers['x-real-ip'] ||
      (req.headers['x-forwarded-for'] as string)?.split(',')[0] ||
      req.ip
    );
  }

  protected async throwThrottlingException(
    context: ExecutionContext,
    throttlerLimitDetail: ThrottlerLimitDetail
  ): Promise<void> {
    const response = context.switchToHttp().getResponse();
    response.header(
      'Retry-After',
      Math.ceil(throttlerLimitDetail.ttl / 1000)
    );
    await super.throwThrottlingException(context, throttlerLimitDetail);
  }
}

Cabeçalhos de resposta

RFC 6585 e rascunhos de cabeçalhos RateLimit informam aos clientes exatamente quando tentar novamente:

// rate-limit.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class RateLimitHeadersInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      tap(() => {
        const res = context.switchToHttp().getResponse();
        const req = context.switchToHttp().getRequest();

        // Values injected by ThrottlerGuard after evaluation
        if (req.rateLimit) {
          res.set({
            'X-RateLimit-Limit': req.rateLimit.limit,
            'X-RateLimit-Remaining': Math.max(
              0,
              req.rateLimit.limit - req.rateLimit.current
            ),
            'X-RateLimit-Reset': new Date(
              Date.now() + req.rateLimit.ttl
            ).toISOString(),
            'RateLimit-Policy': `${req.rateLimit.limit};w=${Math.ceil(
              req.rateLimit.ttl / 1000
            )}`,
          });
        }
      })
    );
  }
}

Estratégias específicas para endpoints

Endpoints diferentes garantem limites diferentes. Aqui está uma tabela de referência para padrões comuns:

Tipo de terminalAlgoritmoLimiteJanela
Login/redefinição de senhaRegistro de janela deslizante515 minutos
Verificação OTP/2FAJanela fixa310 minutos
API de leitura públicaBalde de tokensExplosão de 1000, preenchimento de 100/s
API de mutação (autenticada)Balcão de janela deslizante3001 minuto
Ingestão de webhookJanela fixa10.0001 minuto
Carregamento de arquivoBalde de tokens10 rajadas, preenchimento de 1/s
Pontos finais de IA/LLMJanela fixa201 minuto
Pesquisa (anônima)Janela fixa301 minuto

Scripts Lua Atômicos para Segurança Distribuída

Quando você tem várias réplicas de API, as condições de corrida nas sequências de verificação de incremento podem permitir intermitências acima do limite. Use um script Lua carregado via redis.defineCommand para tornar a verificação e incremento atômica:

// rate-limit.service.ts
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RateLimitService {
  constructor(private readonly redis: Redis) {
    // Define atomic increment+check as a custom Redis command
    this.redis.defineCommand('rateLimitCheck', {
      numberOfKeys: 1,
      lua: `
        local key   = KEYS[1]
        local limit = tonumber(ARGV[1])
        local ttlMs = tonumber(ARGV[2])
        local count = redis.call('INCR', key)
        if count == 1 then
          redis.call('PEXPIRE', key, ttlMs)
        end
        if count > limit then
          return {0, redis.call('PTTL', key)}
        end
        return {1, -1}
      `,
    });
  }

  async isAllowed(
    key: string,
    limit: number,
    windowMs: number
  ): Promise<{ allowed: boolean; retryAfterMs: number }> {
    const result = await (this.redis as any).rateLimitCheck(
      key, limit, windowMs
    ) as [number, number];

    return {
      allowed: result[0] === 1,
      retryAfterMs: result[1] > 0 ? result[1] : 0,
    };
  }
}

Degradação graciosa e estratégias de desvio

A limitação de taxas não deve bloquear verificações de integridade internas, agentes de monitoramento ou parceiros confiáveis.

// trusted-bypass.guard.ts
@Injectable()
export class RateLimitWithBypassGuard extends ThrottlerBehindProxyGuard {
  private readonly trustedTokens = new Set([
    process.env.MONITORING_TOKEN,
    process.env.PARTNER_API_TOKEN,
  ].filter(Boolean));

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();

    // Internal health checks bypass all rate limits
    if (req.path === '/health' || req.path === '/ready') {
      return true;
    }

    // Trusted API tokens bypass
    const token = req.headers['x-bypass-token'];
    if (token && this.trustedTokens.has(token)) {
      return true;
    }

    return super.canActivate(context);
  }
}

Para limitação de taxa progressiva (aviso antes do bloqueio rígido), retorne 429 com um cabeçalho Retry-After somente após ultrapassar 80% do limite:

// In your custom guard, after counting requests:
if (count > limit * 0.9 && count <= limit) {
  response.set('X-RateLimit-Warning', 'approaching limit');
}
if (count > limit) {
  response.set('Retry-After', retryAfterSeconds.toString());
  throw new HttpException('Rate limit exceeded', 429);
}

Limitação de taxa de teste

// rate-limit.spec.ts
describe('Rate Limiting', () => {
  it('should block after limit exceeded', async () => {
    const app = moduleRef.createNestApplication();
    await app.init();

    // Hit the endpoint 5 times (limit for login)
    for (let i = 0; i < 5; i++) {
      await request(app.getHttpServer())
        .post('/auth/login')
        .send({ email: '[email protected]', password: 'wrongpassword' })
        .expect((res) => expect(res.status).toBeLessThan(429));
    }

    // 6th request should be blocked
    const response = await request(app.getHttpServer())
      .post('/auth/login')
      .send({ email: '[email protected]', password: 'wrongpassword' });

    expect(response.status).toBe(429);
    expect(response.headers['retry-after']).toBeDefined();
    expect(response.body.message).toContain('rate limit');
  });
});

Monitoramento e alertas

Eventos de limite de taxa são sinais valiosos. Registre-os em sua plataforma de observabilidade:

@Injectable()
export class RateLimitMetricsService {
  async recordRateLimitHit(userId: string, endpoint: string, ip: string) {
    await this.metricsService.increment('rate_limit.hits', {
      endpoint,
      user_type: userId ? 'authenticated' : 'anonymous',
    });

    // Alert on sustained attacks (>100 hits in 1 min from same IP)
    const alertKey = `rl_alert:${ip}`;
    const recentHits = await this.redis.incr(alertKey);
    if (recentHits === 1) {
      await this.redis.expire(alertKey, 60);
    }

    if (recentHits === 100) {
      await this.alertService.send({
        severity: 'high',
        message: `Rate limit attack detected from IP ${ip}`,
        endpoint,
      });
    }
  }
}

Crie painéis de rastreamento:

  • Limite de taxa de acessos por endpoint (p95, p99)
  • Principais IPs/usuários atingindo limites
  • Porcentagem de solicitações bloqueadas versus atendidas
  • Durações de novas tentativas para detectar clientes mal configurados

Perguntas frequentes

Devo limitar a taxa por IP ou por ID de usuário?

Use ambos. Para endpoints não autenticados, o IP é o único identificador disponível. Para endpoints autenticados, prefira sempre o ID do usuário — é mais preciso e evita que um IP compartilhado (como um NAT corporativo) bloqueie todos os funcionários. Implemente uma verificação de dois níveis: limite de IP no nível Nginx e limite de ID do usuário no nível do aplicativo.

Qual é o código de status HTTP correto para limitação de taxa?

Sempre 429 Too Many Requests de acordo com RFC 6585. Nunca use 503 Service Unavailable (implica falha de infraestrutura) ou 403 Forbidden (implica falha de autorização). Inclua Retry-After como cabeçalho em segundos para que os clientes saibam quando tentar novamente.

Como lidar com a limitação de taxa por trás do Cloudflare ou de um balanceador de carga?

Configure seu proxy para definir X-Real-IP ou CF-Connecting-IP e confie apenas no intervalo de IP do seu proxy. No Nginx: set_real_ip_from 103.21.244.0/22; real_ip_header CF-Connecting-IP;. No NestJS, defina app.set('trust proxy', 1) e leia req.ip que o NestJS resolve a partir do cabeçalho do proxy confiável.

Qual estrutura de dados Redis é melhor para limitação de taxa?

Para contadores de janela fixa/deslizante, use INCR + EXPIRE em uma chave de string — O(1) por solicitação. Para log de janela deslizante, use um conjunto classificado (ZADD, ZREMRANGEBYSCORE, ZCARD) — O(log n). Para token bucket, use um hash (HSET, HGET) — O(1). Os scripts Lua tornam todas as operações atômicas em todos os três padrões.

Como devo lidar com limites de taxa para webhooks de provedores confiáveis?

Stripe, GitHub e provedores semelhantes enviam webhooks de intervalos de IP conhecidos. Mantenha uma lista de permissões de seus intervalos CIDR e ignore a limitação de taxa para esses IPs em seu endpoint de ingestão de webhook. Verifique primeiro a assinatura do webhook – a verificação da assinatura é a sua camada de segurança real, não a limitação da taxa.

Posso implementar a limitação de taxa no nível Nginx?

Sim, e você deve fazer isso para obter proteção básica contra DDoS. Use limit_req_zone no Nginx para limites aproximados baseados em IP (1000 req/min). Coloque a limitação de taxa no nível do aplicativo na parte superior para obter controle granular por usuário e por endpoint. As duas camadas se complementam: o Nginx lida com ataques de volume de maneira barata, sem atingir seu aplicativo, e o NestJS lida com limites de lógica de negócios diferenciados.


Próximas etapas

Construir uma API de produção sem limitação de taxa robusta é como deixar a porta da frente destrancada. Os padrões neste guia – bucket de token para tráfego intermitente, contador de janela deslizante para aplicação suave, armazenamento distribuído apoiado por Redis e respostas 429 adequadas – formam a espinha dorsal de uma API segura e escalonável.

ECOSIRE cria back-ends NestJS de nível empresarial com limitação de taxa, cache Redis e observabilidade total integrados desde o primeiro dia. Se você estiver lançando uma nova API ou fortalecendo uma já existente, explore nossos serviços de engenharia de back-end para ver como podemos acelerar sua entrega.

E

Escrito por

ECOSIRE Research and Development Team

Construindo produtos digitais de nível empresarial na ECOSIRE. Compartilhando insights sobre integrações Odoo, automação de e-commerce e soluções de negócios com IA.

Converse no WhatsApp