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/throttleroferece 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-RemainingeX-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 Requestscom 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 terminal | Algoritmo | Limite | Janela |
|---|---|---|---|
| Login/redefinição de senha | Registro de janela deslizante | 5 | 15 minutos |
| Verificação OTP/2FA | Janela fixa | 3 | 10 minutos |
| API de leitura pública | Balde de tokens | Explosão de 1000, preenchimento de 100/s | — |
| API de mutação (autenticada) | Balcão de janela deslizante | 300 | 1 minuto |
| Ingestão de webhook | Janela fixa | 10.000 | 1 minuto |
| Carregamento de arquivo | Balde de tokens | 10 rajadas, preenchimento de 1/s | — |
| Pontos finais de IA/LLM | Janela fixa | 20 | 1 minuto |
| Pesquisa (anônima) | Janela fixa | 30 | 1 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.
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.
Artigos Relacionados
Cybersecurity Trends 2026-2027: Zero Trust, AI Threats, and Defense
The definitive guide to cybersecurity trends for 2026-2027—AI-powered attacks, zero trust implementation, supply chain security, and building resilient security programs.
Financial Services ERP Implementation: Regulatory and Security Requirements
A practitioner's guide to implementing ERP in regulated financial services firms, covering security controls, compliance validation, data governance, and phased rollout.
Government ERP Implementation: Security Clearance and Compliance
A comprehensive guide to implementing ERP in government agencies, covering FedRAMP compliance, security clearance requirements, change management, and phased deployment.