Rate Limiting: O Conceito Fundamental
Rate limiting é a prática de controlar a quantidade de requisições que um cliente pode fazer a uma API em um período determinado. Imagine uma torneira que libera apenas uma quantidade máxima de água por minuto — se alguém tentar abrir completamente, o sistema continua liberando apenas aquela quantidade pré-definida. No contexto de APIs, isso protege seus servidores contra sobrecargas, ataques de negação de serviço (DoS) e uso abusivo dos recursos.
O conceito é simples em teoria, mas exige implementação cuidadosa em produção. Você precisa decidir: quantas requisições permitir? Por quanto tempo? Como contar essas requisições? A resposta depende de sua arquitetura, tipo de cliente (público vs. autenticado) e recursos disponíveis. Uma API pública pode permitir 100 requisições por minuto para usuários anônimos, enquanto clientes premium recebem 10.000 requisições por hora.
Algoritmos Comuns de Rate Limiting
Existem várias estratégias para implementar rate limiting, cada uma com trade-offs diferentes. O Token Bucket é o mais popular: imagina um balde que recebe tokens a uma taxa constante. Cada requisição consome um token, e se o balde estiver vazio, a requisição é rejeitada. Esse algoritmo permite rajadas controladas — se ninguém fez requisições por 10 minutos, o balde fica cheio e o próximo cliente pode usar vários tokens de uma vez.
O Sliding Window é mais preciso, mas computacionalmente mais caro. Ele mantém um registro de todas as requisições em uma janela de tempo móvel. Se você permite 100 requisições por minuto, o sistema verifica se há 100 requisições nos últimos 60 segundos. O Fixed Window é mais simples (reseta a cada minuto), mas permite picos na borda das janelas — alguém pode fazer 100 requisições no segundo 59 e mais 100 no segundo 61, dobrando o limite.
Implementação Prática com Node.js e Redis
A forma mais eficiente de implementar rate limiting em sistemas distribuídos é usando Redis, um armazenamento em memória que fornece operações atômicas. Vou mostrar uma implementação real com Express e Redis usando o algoritmo Token Bucket.
const express = require('express');
const redis = require('redis');
const { promisify } = require('util');
const app = express();
const client = redis.createClient();
// Promisificar comandos Redis para usar async/await
const incr = promisify(client.incr).bind(client);
const expire = promisify(client.expire).bind(client);
const get = promisify(client.get).bind(client);
// Middleware de Rate Limiting
const rateLimitMiddleware = async (req, res, next) => {
const clientId = req.ip; // Ou usar req.user.id se autenticado
const limit = 100; // Requisições por minuto
const window = 60; // Segundos
const key = `rate_limit:${clientId}`;
try {
const current = await incr(key);
// Na primeira requisição, define o tempo de expiração
if (current === 1) {
await expire(key, window);
}
// Headers informativos
res.set('X-RateLimit-Limit', limit);
res.set('X-RateLimit-Remaining', Math.max(0, limit - current));
res.set('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + window);
if (current > limit) {
return res.status(429).json({
error: 'Too Many Requests',
retryAfter: window,
message: `Limite de ${limit} requisições por minuto excedido`
});
}
next();
} catch (err) {
console.error('Erro no rate limit:', err);
// Em caso de erro, permite a requisição (fail-open)
next();
}
};
app.get('/api/data', rateLimitMiddleware, (req, res) => {
res.json({ message: 'Dados da API' });
});
app.listen(3000, () => {
console.log('Servidor rodando na porta 3000');
});
Este código implementa um sistema simples mas eficaz. Cada cliente tem uma chave no Redis que incrementa com cada requisição. A chave expira automaticamente após o window definido, "resetando" o contador. Os headers HTTP informam ao cliente quanto tempo falta para sua cota se renovar — isso é importante para uma boa experiência de desenvolvedor usando sua API.
Estratégia de Tiered Rate Limiting
Em sistemas reais, você raramente tem um único limite. Clientes pagos merecem limites maiores, e você quer proteger endpoints críticos com limites mais apertados. Aqui está uma implementação mais sofisticada:
const getTierLimits = (user) => {
// Retorna limite baseado no tipo de conta
if (!user) return { limit: 50, window: 60 }; // Anônimo
if (user.tier === 'premium') return { limit: 10000, window: 3600 }; // Por hora
if (user.tier === 'enterprise') return { limit: 100000, window: 3600 };
return { limit: 500, window: 3600 }; // Padrão
};
const tieredRateLimitMiddleware = async (req, res, next) => {
const user = req.user || null;
const { limit, window } = getTierLimits(user);
const endpoint = req.path;
// Rate limit diferente por endpoint
const endpointMultiplier = endpoint.includes('/search') ? 0.5 : 1;
const adjustedLimit = Math.floor(limit * endpointMultiplier);
const key = `rate_limit:${user?.id || req.ip}:${endpoint}`;
try {
const current = await incr(key);
if (current === 1) {
await expire(key, window);
}
res.set('X-RateLimit-Limit', adjustedLimit);
res.set('X-RateLimit-Remaining', Math.max(0, adjustedLimit - current));
if (current > adjustedLimit) {
return res.status(429).json({
error: 'Limite de requisições excedido',
tier: user?.tier || 'anonymous',
upgrade: 'Considere fazer upgrade para aumentar seus limites'
});
}
next();
} catch (err) {
next();
}
};
app.use('/api/', tieredRateLimitMiddleware);
Essa abordagem permite que endpoints custosos (como /search) tenham limites mais apertados, enquanto clientes premium obtêm vantagem competitiva real. É justo e aumenta a monetização da API.
Throttling: Controle de Velocidade Sem Rejeição
Enquanto rate limiting rejeita requisições que excedem o limite, throttling simplesmente desacelera o processamento. Em vez de retornar erro 429, você coloca a requisição em fila e a processa gradualmente. Isso é mais amigável para clientes legítimos que precisam fazer muitas requisições, mas não é agressivo.
Throttling é especialmente útil em cenários onde você quer ser generoso com clientes, mas proteger seu banco de dados de picos. Por exemplo, um cliente pode fazer 1000 requisições, mas você as processa a uma taxa máxima de 10 por segundo, levando 100 segundos no total. O cliente aguarda, mas eventualmente consegue seus dados.
Implementando Throttling com Fila de Prioridade
const Queue = require('bull');
const express = require('express');
const app = express();
const apiQueue = new Queue('api-requests', {
redis: { host: 'localhost', port: 6379 }
});
// Processa requisições a uma taxa controlada
apiQueue.process(async (job) => {
const { clientId, endpoint, params } = job.data;
// Simula processamento da requisição real
console.log(`Processando para ${clientId}: ${endpoint}`);
// Aqui você faria a chamada real à sua lógica de negócio
return { success: true, data: `Resposta para ${endpoint}` };
});
// Limita concorrência — processa apenas 10 requisições por segundo
apiQueue.process(10, async (job) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(job.data);
}, 100); // 100ms entre requisições = ~10 por segundo
});
});
app.post('/api/process', async (req, res) => {
try {
// Adiciona à fila em vez de processar imediatamente
const job = await apiQueue.add(
{
clientId: req.ip,
endpoint: req.path,
params: req.body
},
{
attempts: 3, // Tenta 3 vezes se falhar
backoff: {
type: 'exponential',
delay: 2000
},
removeOnComplete: true
}
);
res.status(202).json({
message: 'Requisição enfileirada para processamento',
jobId: job.id,
status: 'pending'
});
} catch (err) {
res.status(500).json({ error: 'Falha ao enfileirar' });
}
});
// Endpoint para verificar status de uma requisição
app.get('/api/status/:jobId', async (req, res) => {
const job = await apiQueue.getJob(req.params.jobId);
if (!job) {
return res.status(404).json({ error: 'Job não encontrado' });
}
const state = await job.getState();
const progress = job.progress();
res.json({
id: job.id,
status: state,
progress: progress,
data: job.data
});
});
app.listen(3000);
Essa implementação usa uma fila (Bull/Redis) para gerenciar requisições. Clientes não recebem rejeição, mas ficam na fila. Você controla exatamente quantas requisições são processadas por segundo, protegendo seus recursos. É especialmente útil para operações custosas como geração de relatórios ou processamento de imagens.
Proteção contra Abuso: Detecção Comportamental
Rate limiting e throttling são defesas passivas — você limita quantidade. Proteção contra abuso exige detecção ativa de padrões suspeitos. Um único IP fazendo 100 requisições por segundo é óbvio. Mas 10 IPs diferentes, cada um fazendo 20 requisições por minuto, em perfeita sincronia, pode ser um ataque distribuído — mais difícil de detectar com simples contagem.
Proteção contra abuso envolve análise de padrões: velocidade de requisições, distribuição geográfica dos IPs, tipos de endpoints acessados, taxa de erro, e até mesmo assinatura do User-Agent. Você cria um "escore de risco" para cada cliente e toma ações progressivas — primeiro um aviso, depois degradação de serviço, finalmente bloqueio.
Implementando Detecção de Anomalias
const redis = require('redis');
const geoip = require('geoip-lite');
const client = redis.createClient();
const { promisify } = require('util');
const get = promisify(client.get).bind(client);
const set = promisify(client.set).bind(client);
const hget = promisify(client.hget).bind(client);
const hset = promisify(client.hset).bind(client);
const hincrby = promisify(client.hincrby).bind(client);
const calculateAbuseScore = async (req) => {
const clientIp = req.ip;
const geo = geoip.lookup(clientIp);
let score = 0;
// 1. Verificar velocidade anormal (muitas requisições rapidamente)
const recentKey = `recent:${clientIp}`;
const recentCount = await get(recentKey) || 0;
if (recentCount > 50) { // 50 requisições em 10 segundos
score += 30;
}
// 2. Analisar mudanças geográficas impossíveis
const lastGeoKey = `geo:${clientIp}`;
const lastGeo = await get(lastGeoKey);
if (lastGeo) {
const lastGeoObj = JSON.parse(lastGeo);
const timeDiff = Date.now() - lastGeoObj.timestamp;
const distance = calculateDistance(
lastGeoObj.lat,
lastGeoObj.lon,
geo.ll[0],
geo.ll[1]
);
// Impossível se mover mais de 900 km em menos de 1 segundo
if (distance > 900 && timeDiff < 1000) {
score += 40;
}
}
await set(lastGeoKey, JSON.stringify({
lat: geo.ll[0],
lon: geo.ll[1],
timestamp: Date.now()
}), 'EX', 3600);
// 3. Verificar padrão de erro (muitos 4xx)
const errorKey = `errors:${clientIp}`;
const errorCount = await hget(errorKey, Date.now());
if (errorCount > 20) { // Mais de 20 erros em um minuto
score += 25;
}
// 4. User-Agent suspeito (bots conhecidos)
const userAgent = req.get('user-agent') || '';
const suspiciousAgents = ['curl', 'wget', 'python', 'scrapy'];
if (suspiciousAgents.some(agent => userAgent.toLowerCase().includes(agent))) {
score += 15;
}
// 5. Falta de headers padrão
if (!req.get('accept') || !req.get('accept-language')) {
score += 10;
}
return score;
};
const calculateDistance = (lat1, lon1, lat2, lon2) => {
// Haversine formula (distância em km)
const R = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
};
const abuseDetectionMiddleware = async (req, res, next) => {
try {
const score = await calculateAbuseScore(req);
const clientIp = req.ip;
// Ações baseadas em score
if (score >= 80) {
// Bloqueio imediato
return res.status(403).json({
error: 'Acesso negado',
message: 'Seu acesso foi bloqueado devido a atividade suspeita'
});
}
if (score >= 50) {
// Throttle agresivo
res.set('X-Abuse-Score', score);
// Delay de 2 segundos antes de processar
return setTimeout(() => next(), 2000);
}
if (score >= 30) {
// Aviso
res.set('X-Abuse-Warning', 'Continue assim e será bloqueado');
next();
} else {
next();
}
} catch (err) {
console.error('Erro na detecção de abuso:', err);
next(); // Fail-open
}
};
const express = require('express');
const app = express();
app.use(abuseDetectionMiddleware);
app.get('/api/data', (req, res) => {
res.json({ data: 'Dados protegidos' });
});
app.listen(3000);
Esse código implementa um sistema de pontuação (score) para detectar abuso. Cada comportamento suspeito adiciona pontos. Quanto maior a pontuação, mais severas as ações: aviso, throttle, ou bloqueio. É um sistema de defesa em camadas — não bloqueia por padrão, mas reage proporcionalmente à suspeita.
Whitelist e Trusted Clients
Nem todos os clientes merecem as mesmas restrições. APIs públicas devem ter um mecanismo de whitelist para clientes confiáveis ou críticos:
const isWhitelisted = async (clientId) => {
const whitelist = await get(`whitelist:${clientId}`);
return !!whitelist;
};
const abuseProtectionMiddleware = async (req, res, next) => {
const clientId = req.user?.id || req.ip;
// Clientes whitelistados pulam todas as verificações
if (await isWhitelisted(clientId)) {
return next();
}
// Caso contrário, aplica proteções
const score = await calculateAbuseScore(req);
if (score >= 80) {
return res.status(403).json({ error: 'Acesso negado' });
}
next();
};
Conclusão
Você aprendeu que rate limiting, throttling e detecção de abuso são camadas complementares de proteção. Rate limiting é sua defesa passiva — diz "não" a requisições além do limite. Throttling é sua abordagem amigável — aceita requisições mas as processa lentamente. Detecção de abuso é sua inteligência — identifica padrões suspeitos antes que virem problema.
A implementação prática requer escolher as ferramentas certas (Redis para sistemas distribuídos é praticamente obrigatório), entender trade-offs (precisão vs. performance, justiça vs. segurança), e monitorar constantemente. Uma API bem protegida é aquela que rejeita ataques, acomoda clientes legítimos, e oferece upgrading para quem precisa de mais capacidade. Não é apenas engenharia — é produto.
Referências
- Redis Rate Limiting Pattern - Documentação oficial do Redis sobre padrões de rate limiting
- HTTP Status Code 429 (Too Many Requests) - MDN Web Docs
- Bull Queue Documentation - Biblioteca Node.js para filas distribuídas
- OWASP API Security - Projeto de segurança de APIs da OWASP
- The Art of API Design - Rate Limiting Best Practices - Blog da Stripe sobre implementação em produção