Rate Limiting em Produção: Algoritmos, Trade-offs e Implementação
Protegendo suas APIs de abuso e sobrecarga enquanto equilibra experiência do usuário com estabilidade do sistema.
Resumo executivo
Protegendo suas APIs de abuso e sobrecarga enquanto equilibra experiência do usuário com estabilidade do sistema.
Ultima atualizacao: 12/03/2026
A necessidade de rate limiting
Toda API pública será abusada. Isso não é especulação—é a realidade operacional de expor serviços na internet. Scrapers, bots automatizados, atores maliciosos e até usuários bem-intencionados enviarão mais requisições do que sua infraestrutura consegue suportar.
Rate limiting serve três propósitos críticos:
- Proteção contra abuso: Prevenir ataques de força bruta, credential stuffing e scraping de API
- Estabilidade de infraestrutura: Garantir que serviços não entrem em colapso sob picos inesperados de carga
- Equidade: Distribuir capacidade equitativamente entre usuários legítimos
O desafio é implementar rate limiting que proteja seus sistemas sem bloquear usuários legítimos ou criar má experiência do usuário. O algoritmo certo, estratégia de armazenamento e configuração importam significativamente.
Algoritmos de rate limiting
Diferentes algoritmos resolvem diferentes problemas. Entender seus trade-offs é essencial para escolher a abordagem certa.
Algoritmo Token Bucket
O algoritmo token bucket permite rajadas (bursts) de requisições até uma capacidade máxima enquanto impõe uma taxa média de longo prazo.
Como funciona:
- Tokens são adicionados a um bucket a uma taxa fixa
- Cada requisição consome um token
- Se o bucket estiver vazio, requisições são rejeitadas
- O bucket tem uma capacidade máxima (permissão de burst)
typescriptclass TokenBucketRateLimiter {
constructor(
private capacity: number, // Máximo de tokens
private refillRate: number, // Tokens por segundo
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
private tokens: number;
private lastRefill: number;
async tryRequest(): Promise<boolean> {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return true;
}
return false;
}
private refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
getRemaining(): number {
this.refill();
return this.tokens;
}
}
// Uso: 100 requisições por segundo, burst de 200
const rateLimiter = new TokenBucketRateLimiter(200, 100);Quando usar: APIs que precisam permitir tráfego em rajada (ex: uploads de arquivo, operações em lote) enquanto mantêm limites de taxa globais.
Vantagens:
- Permite bursts legítimos
- Comportamento de throttling suave
- Eficiente em memória
Desvantagens:
- Tokens podem acumular e habilitar bursts grandes se subutilizados
Algoritmo Leaky Bucket
O leaky bucket processa requisições a uma taxa constante, enfileirando requisições excedentes até a fila transbordar.
Como funciona:
- Requisições são adicionadas a uma fila
- Requisições deixam a fila a uma taxa fixa
- Se a fila estiver cheia, requisições são rejeitadas
typescriptclass LeakyBucketRateLimiter {
constructor(
private queueSize: number, // Tamanho máximo da fila
private leakRate: number, // Requisições por segundo
) {
this.queue = [];
this.lastLeak = Date.now();
}
private queue: any[];
private lastLeak: number;
async tryRequest(): Promise<boolean> {
this.leak();
if (this.queue.length < this.queueSize) {
this.queue.push(Date.now());
return true;
}
return false;
}
private leak() {
const now = Date.now();
const elapsed = (now - this.lastLeak) / 1000;
const requestsToLeak = Math.floor(elapsed * this.leakRate);
this.queue.splice(0, requestsToLeak);
this.lastLeak = now;
}
}
// Uso: Processar 50 requisições por segundo, enfileirar até 100
const rateLimiter = new LeakyBucketRateLimiter(100, 50);Quando usar: APIs que precisam de taxas de saída estritas e consistentes (ex: filas de mensagens, streaming de dados).
Vantagens:
- Taxa de saída consistente
- Lida bem com suavização de tráfego
Desvantagens:
- Nenhuma permissão de burst
- Complexidade de gerenciamento de fila
Algoritmo Sliding Window Log
O sliding window log rastreia cada requisição dentro de uma janela de tempo, fornecendo rate limiting preciso.
Como funciona:
- Armazena timestamp de cada requisição
- Conta requisições dentro da janela de tempo deslizante
- Rejeita se a contagem exceder o limite
typescriptclass SlidingWindowLogRateLimiter {
constructor(
private maxRequests: number,
private windowMs: number,
) {
this.requests = [];
}
private requests: number[];
async tryRequest(): Promise<boolean> {
const now = Date.now();
const windowStart = now - this.windowMs;
// Remover requisições fora da janela
this.requests = this.requests.filter(t => t > windowStart);
if (this.requests.length < this.maxRequests) {
this.requests.push(now);
return true;
}
return false;
}
getRetryAfter(): number {
const oldestRequest = this.requests[0];
const windowStart = Date.now() - this.windowMs;
return Math.max(0, oldestRequest - windowStart);
}
}
// Uso: Máximo 100 requisições por 60 segundos
const rateLimiter = new SlidingWindowLogRateLimiter(100, 60000);Quando usar: APIs que exigem rate limiting preciso por janela com headers retry-after amigáveis ao usuário.
Vantagens:
- Rate limiting preciso
- Fácil calcular retry-after
Desvantagens:
- Intensivo em memória para tráfego alto
- Overhead de desempenho do gerenciamento de log
Algoritmo Fixed Window Counter
O fixed window counter divide tempo em intervalos fixos e reseta nos limites de intervalo.
Como funciona:
- Divide tempo em janelas fixas (ex: por minuto)
- Incrementa contador para cada requisição
- Reseta contador no limite da janela
typescriptclass FixedWindowRateLimiter {
constructor(
private maxRequests: number,
private windowMs: number,
) {
this.currentWindow = Math.floor(Date.now() / this.windowMs);
this.requestCount = 0;
}
private currentWindow: number;
private requestCount: number;
async tryRequest(): Promise<boolean> {
const now = Date.now();
const windowNumber = Math.floor(now / this.windowMs);
if (windowNumber !== this.currentWindow) {
this.currentWindow = windowNumber;
this.requestCount = 0;
}
if (this.requestCount < this.maxRequests) {
this.requestCount++;
return true;
}
return false;
}
getRetryAfter(): number {
const now = Date.now();
const currentWindow = Math.floor(now / this.windowMs);
const nextWindow = currentWindow + 1;
const nextWindowStart = nextWindow * this.windowMs;
return Math.max(0, nextWindowStart - now);
}
}
// Uso: Máximo 1000 requisições por minuto
const rateLimiter = new FixedWindowRateLimiter(1000, 60000);Quando usar: Cenários simples de rate limiting onde imprecisões menores nos limites da janela são aceitáveis.
Vantagens:
- Implementação simples
- Overhead de memória mínimo
Desvantagens:
- Pico nos limites da janela (taxa dupla possível)
- Menos preciso que sliding window
Comparação de algoritmos
| Algoritmo | Precisão | Suporte a Burst | Memória | Complexidade | Melhor Para |
|---|---|---|---|---|---|
| Token Bucket | Alta | Sim | Baixa | Média | APIs que precisam de permissão de burst |
| Leaky Bucket | Média | Não | Média | Média | Taxas de saída consistentes |
| Sliding Window | Muito Alta | Limitado | Alta | Alta | Limites precisos por janela |
| Fixed Window | Baixa | Não | Baixa | Baixa | Rate limiting simples |
Rate limiting distribuído
Em arquiteturas de microserviços, rate limiting deve funcionar através de múltiplas instâncias. Isso introduz dois desafios:
Backend de armazenamento
Estado de rate limiting deve ser armazenado em backend compartilhado e distribuído:
typescript// Implementação sliding window baseada em Redis
import { createClient } from 'redis';
const redis = createClient();
class DistributedSlidingWindowRateLimiter {
constructor(
private maxRequests: number,
private windowMs: number,
) {}
async tryRequest(key: string): Promise<boolean> {
const now = Date.now();
const windowStart = now - this.windowMs;
const pipeline = redis.multi();
// Remover requisições antigas
pipeline.zRemRangeByScore(key, 0, windowStart);
// Contar requisições atuais
pipeline.zCard(key);
// Adicionar nova requisição
pipeline.zAdd(key, { score: now, value: now.toString() });
// Definir expiração
pipeline.expire(key, Math.ceil(this.windowMs / 1000));
const results = await pipeline.exec();
const count = results[1] as number;
return count < this.maxRequests;
}
}Overhead de sincronização
Rate limiting distribuído introduz latência de rede em cada requisição. Mitigue isso com:
- Redis cluster: Use Redis Cluster para escalabilidade horizontal
- Cache local: Armazene decisões de rate limit localmente para janelas sub-segundo
- Atualizações assíncronas: Atualize estado de rate limit de forma assíncrona
Estratégias de rate limiting por caso de uso
Rate limiting de endpoint de API
typescript// Middleware Express para rate limiting de endpoint
import { Router } from 'express';
const router = Router();
// Limites diferentes para diferentes endpoints
const publicApiLimiter = new TokenBucketRateLimiter(100, 10);
const authApiLimiter = new TokenBucketRateLimiter(20, 2);
const premiumApiLimiter = new TokenBucketRateLimiter(1000, 100);
router.use('/api/public', async (req, res, next) => {
const allowed = await publicApiLimiter.tryRequest();
if (!allowed) {
return res.status(429).json({ error: 'Limite de taxa excedido' });
}
next();
});
router.use('/api/auth', async (req, res, next) => {
const allowed = await authApiLimiter.tryRequest();
if (!allowed) {
const retryAfter = authApiLimiter.getRetryAfter();
res.setHeader('Retry-After', Math.ceil(retryAfter / 1000));
return res.status(429).json({ error: 'Limite de taxa excedido' });
}
next();
});Rate limiting baseado em usuário
typescript// Rate limit por ID de usuário ou chave de API
class UserRateLimiter {
private limiters: Map<string, RateLimiter> = new Map();
async tryRequest(userId: string): Promise<boolean> {
if (!this.limiters.has(userId)) {
this.limiters.set(userId, new SlidingWindowLogRateLimiter(100, 60000));
}
const limiter = this.limiters.get(userId)!;
return await limiter.tryRequest();
}
}Rate limiting baseado em IP
typescript// Rate limit por endereço IP (use com cautela)
import ip from 'ip';
class IpRateLimiter {
private limiters: Map<string, RateLimiter> = new Map();
async tryRequest(reqIp: string): Promise<boolean> {
// Normalizar endereços IP (tratar IPv6 vs IPv4)
const normalizedIp = ip.normalize(reqIp);
if (!this.limiters.has(normalizedIp)) {
this.limiters.set(normalizedIp, new SlidingWindowLogRateLimiter(50, 60000));
}
const limiter = this.limiters.get(normalizedIp)!;
return await limiter.tryRequest();
}
}Cautela: Rate limiting baseado em IP tem problemas com NAT, proxies e redes compartilhadas. Use como medida de defesa em profundidade, não como estratégia primária.
Headers de resposta e experiência do usuário
Rate limiting deve ser transparente para clientes através de headers HTTP apropriados:
typescriptfunction setRateLimitHeaders(res: Response, limiter: RateLimiter) {
const remaining = limiter.getRemaining();
const limit = limiter.getLimit();
const reset = limiter.getResetTime();
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining));
res.setHeader('X-RateLimit-Reset', reset);
if (remaining <= 0) {
const retryAfter = limiter.getRetryAfter();
res.setHeader('Retry-After', Math.ceil(retryAfter / 1000));
}
}Headers padrão:
X-RateLimit-Limit: Máximo de requisições por janelaX-RateLimit-Remaining: Requisições restantes na janela atualX-RateLimit-Reset: Timestamp Unix quando a janela resetaRetry-After: Segundos até retry (quando limite excedido)
Tiers de rate limiting
Implemente rate limiting em camadas baseado em assinatura de usuário ou chave de API:
typescriptinterface RateLimitTier {
requestsPerMinute: number;
burstCapacity: number;
}
const rateLimitTiers: Record<string, RateLimitTier> = {
free: { requestsPerMinute: 10, burstCapacity: 20 },
basic: { requestsPerMinute: 100, burstCapacity: 200 },
pro: { requestsPerMinute: 1000, burstCapacity: 2000 },
enterprise: { requestsPerMinute: 10000, burstCapacity: 20000 },
};
class TieredRateLimiter {
private limiters: Map<string, TokenBucketRateLimiter> = new Map();
async tryRequest(userId: string, tier: string): Promise<boolean> {
if (!this.limiters.has(userId)) {
const config = rateLimitTiers[tier] || rateLimitTiers.free;
this.limiters.set(userId, new TokenBucketRateLimiter(
config.burstCapacity,
config.requestsPerMinute / 60
));
}
const limiter = this.limiters.get(userId)!;
return await limiter.tryRequest();
}
}Anti-padrões comuns
Anti-padrão 1: Rate limiting apenas na borda
Rate limiting no nível de CDN ou API Gateway é insuficiente para proteção contra abuso interno ou contas comprometidas. Implemente rate limiting em nível de aplicação também.
Anti-padrão 2: Bloquear tráfego de burst legítimo
Usar algoritmos de janela fixa para APIs que legitimamente experimentam tráfego em burst (ex: após usuário completar envio de formulário) cria má experiência do usuário. Use algoritmos de token bucket ou sliding window em vez disso.
Anti-padrão 3: Ignorar headers de rate limit
Clientes que ignoram headers de rate limit e fazem retry imediatamente criam efeitos de thundering herd. Implemente backoff exponencial com jitter no lado do cliente.
Anti-padrão 4: Rate limiting sem monitoramento
Rate limiting sem monitoramento abrangente significa que você não saberá se usuários legítimos estão sendo bloqueados ou se atacantes estão encontrando formas de contornar seus limites.
Monitoramento e observabilidade
Rastreie métricas de rate limiting para garantir que sua estratégia está funcionando:
typescript// Métricas Prometheus para rate limiting
import { Counter, Histogram, Gauge } from 'prom-client';
export const rateLimitMetrics = {
requestsAllowed: new Counter({
name: 'rate_limit_requests_allowed_total',
help: 'Total de requisições permitidas pelo rate limiter',
labelNames: ['endpoint', 'user_id'],
}),
requestsDenied: new Counter({
name: 'rate_limit_requests_denied_total',
help: 'Total de requisições negadas pelo rate limiter',
labelNames: ['endpoint', 'user_id'],
}),
requestDuration: new Histogram({
name: 'rate_limit_check_duration_seconds',
help: 'Tempo gasto verificando rate limit',
buckets: [0.001, 0.005, 0.01, 0.05, 0.1],
}),
remainingTokens: new Gauge({
name: 'rate_limit_remaining_tokens',
help: 'Tokens restantes para rate limiter',
labelNames: ['user_id'],
}),
};
// Uso no rate limiter
if (await limiter.tryRequest(userId)) {
rateLimitMetrics.requestsAllowed.labels({ endpoint, user_id: userId }).inc();
} else {
rateLimitMetrics.requestsDenied.labels({ endpoint, user_id: userId }).inc();
}Conclusão
Rate limiting não é opcional para APIs públicas—é um mecanismo fundamental de segurança e estabilidade. O algoritmo certo depende do seu caso de uso:
- Token bucket: APIs que precisam de permissão de burst
- Leaky bucket: APIs que exigem taxas de saída consistentes
- Sliding window: APIs que precisam de limites precisos por janela
- Fixed window: Cenários simples com requisitos de precisão toleráveis
Implemente rate limiting em múltiplas camadas (borda, aplicação, por usuário), use armazenamento distribuído para consistência e forneça headers claros de rate limit aos clientes. Monitore continuamente suas métricas de rate limiting para garantir que usuários legítimos não estejam sendo bloqueados enquanto atacantes são efetivamente impedidos.
Suas APIs estão experimentando abuso ou problemas de estabilidade devido a tráfego não controlado? Fale com especialistas técnicos da Imperialis para projetar e implementar uma estratégia abrangente de rate limiting que proteja sua infraestrutura enquanto mantém uma ótima experiência do usuário.
Fontes
- Algoritmos de Rate Limiting - Cloudflare — comparação de algoritmos
- Redis Rate Limiting - Redis.io — implementação distribuída
- Best Practices de Rate Limiting de API - AWS — padrões de produção