Rate Limiting: A Diferença Entre API Protegida e API Vulnerável
Bots, integrações mal feitas, loops sem querer e ataques: sem limite de requisições, um único cliente consegue deixar o sistema lento para todo mundo. Como implementar rate limiting do jeito certo.
Renan Montanari
Engenheiro de Software Full-Stack
Introdução: Quando Sua API Vira Arma Contra Você
Seu sistema está em pé há meses. Tudo funcionando. Aí um dia, sem motivo aparente, tudo fica lento.
Não é erro. Não é acidente. É alguém, ou alguma coisa, fazendo requisições tão rápido que sua API não consegue respirar.
Pode ser:
- Um bot testando vulnerabilidades
- Um cliente com integração ruim que dispara 100 requisições em paralelo
- Um script de scraping copiando seu catálogo
- Um estagiário rodando um loop infinito sem querer
- Um ataque deliberado
E aqui está o problema: sua API tentou responder a todas as requisições.
Os usuários legítimos? Ficaram esperando na fila.
A diferença entre uma API profissional e uma amadora é simples: a profissional diz "não" quando precisa. A amadora tenta responder tudo e acaba falhando para todo mundo.
Por Que Ninguém Implementa Rate Limiting No Começo
O Cenário Comum
Você começa. A API é rápida. 100 usuários, tudo certo. Sem rate limiting, porque:
- "Por enquanto funciona": não tem lentidão, então não precisa de limite
- "É complexo": parece coisa de empresa grande
- "Não dá tempo": sprint cheio, fica para depois
- "Ninguém vai atacar": confiança demais
Aí vocês chegam a 1.000 usuários. A integração ruim de um cliente começa a fazer 1.000 requisições por minuto. E todo mundo congela.
O Que Pode Acontecer Sem Rate Limiting
Cenário 1: Bot de Scraping
Um bot começa a chamar seu endpoint de produtos. 10 por segundo. 100. 1.000.
Seu servidor tenta responder a cada um. E tenta. E tenta.
Conexões ficam presas. A memória sobe. A CPU dispara. O banco de dados começa a recusar conexões porque atingiu o limite.
O usuário legítimo tenta entrar. Timeout. Tudo congela.
Cenário 2: Integração Ruim
Um cliente integra sua API e faz uma requisição a cada tecla que o usuário dele digita.
Usuário digitando rápido? 10 requisições por segundo.
Multiplica por 500 usuários do cliente ao mesmo tempo: 5.000 requisições por segundo.
Seu banco foi dimensionado para 500.
Tudo fica lento. Não só para aquele cliente: para todo mundo.
Cenário 3: Ataque Deliberado
Alguém aponta centenas de máquinas para o seu sistema, cada uma fazendo milhares de requisições por segundo.
Sem nenhuma camada de proteção, seu servidor tenta responder a cada uma.
Você fica fora do ar por horas.
A Solução: Rate Limiting Bem Feito
Rate limiting não é "bloquear usuário". É distribuir de forma justa.
É você dizer: "cada usuário pode fazer X requisições a cada Y segundos. Passou disso, recebe 429 Too Many Requests e tenta de novo depois."
1️⃣ O Padrão: Token Bucket
A ideia é simples: cada usuário tem um balde de fichas. Cada requisição gasta uma ficha. O balde se reabastece num ritmo constante. Balde vazio, requisição recusada.
Isso permite rajadas curtas (o balde cheio) sem permitir abuso contínuo (o ritmo de reposição).
// ✅ Algoritmo Token Bucket (em memória)
interface Bucket {
tokens: number; // fichas disponíveis agora
lastRefill: number; // timestamp da última reposição
}
class TokenBucketLimiter {
private buckets = new Map<string, Bucket>();
constructor(
private capacity = 100, // rajada máxima: 100 requisições seguidas
private refillPerSecond = 1, // depois disso, 1 por segundo (60/min)
) {}
isAllowed(userId: string): boolean {
const now = Date.now();
const bucket = this.buckets.get(userId) ?? {
tokens: this.capacity, // primeiro acesso: balde cheio
lastRefill: now,
};
// Repõe as fichas proporcionais ao tempo que passou
const secondsPassed = (now - bucket.lastRefill) / 1000;
bucket.tokens = Math.min(
this.capacity,
bucket.tokens + secondsPassed * this.refillPerSecond,
);
bucket.lastRefill = now;
this.buckets.set(userId, bucket);
if (bucket.tokens >= 1) {
bucket.tokens -= 1; // gasta uma ficha
return true;
}
return false; // balde vazio: recusa
}
}
// Uso
const limiter = new TokenBucketLimiter(100, 1);
app.get("/api/products", (req, res) => {
if (!limiter.isAllowed(req.user.id)) {
res.setHeader("Retry-After", "1");
return res.status(429).json({
error: "Too Many Requests",
message: "Você fez requisições demais em pouco tempo. Tente novamente em instantes.",
});
}
res.json(products);
});Serve para entender o conceito e para sistemas com um único servidor. Mas tem dois limites: o Map cresce para sempre (precisa de limpeza periódica) e cada servidor tem sua própria contagem.
2️⃣ Rate Limiting com Redis (Produção)
Com três servidores atrás de um load balancer, o limite em memória vira três limites. O usuário que deveria fazer 100 requisições faz 300.
Solução: guardar a contagem num lugar compartilhado. Redis é a escolha natural.
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
type RateLimitResult = {
allowed: boolean;
limit: number;
remaining: number;
resetSeconds: number;
};
class RedisRateLimiter {
constructor(
private name: string, // separa os contadores de cada regra
private capacity = 100,
private windowMs = 60_000, // 1 minuto
) {}
async check(identifier: string): Promise<RateLimitResult> {
const key = `ratelimit:${this.name}:${identifier}`;
// Incrementa e lê o tempo restante numa única ida ao Redis
const [[, count], [, ttl]] = (await redis
.multi()
.incr(key)
.pttl(key)
.exec()) as [[null, number], [null, number]];
// Chave sem expiração (primeira requisição da janela):
// define agora. Se isso falhar, a próxima requisição corrige,
// e a chave nunca fica "eterna" bloqueando o usuário
if (ttl === -1) {
await redis.pexpire(key, this.windowMs);
}
const resetMs = ttl > 0 ? ttl : this.windowMs;
return {
allowed: count <= this.capacity,
limit: this.capacity,
remaining: Math.max(0, this.capacity - count),
resetSeconds: Math.ceil(resetMs / 1000),
};
}
}
// Middleware reutilizável
const rateLimit =
(limiter: RedisRateLimiter, getId: (req: Request) => string) =>
async (req: Request, res: Response, next: NextFunction) => {
const result = await limiter.check(getId(req));
// Headers no padrão popularizado pela API do GitHub
res.setHeader("X-RateLimit-Limit", result.limit);
res.setHeader("X-RateLimit-Remaining", result.remaining);
res.setHeader(
"X-RateLimit-Reset",
Math.floor(Date.now() / 1000) + result.resetSeconds,
);
if (!result.allowed) {
res.setHeader("Retry-After", result.resetSeconds);
return res.status(429).json({
error: "Too Many Requests",
message: "Você atingiu o limite de requisições.",
retryAfter: result.resetSeconds,
});
}
next();
};
app.use(
"/api/",
rateLimit(new RedisRateLimiter("api", 100, 60_000), (req) => req.user.id),
);Esse é um contador por janela fixa, mais simples que o token bucket. A troca: na virada da janela, o usuário consegue até o dobro do limite em poucos segundos. Para a maioria dos sistemas, é aceitável. Se não for, o token bucket também pode rodar no Redis com um script Lua, ou você pode usar uma biblioteca pronta como rate-limiter-flexible.
3️⃣ Limites Diferentes Por Endpoint
Nem todo endpoint merece o mesmo limite.
- Login: limite baixo (proteção contra tentativa de senha em massa)
- Listar produtos: limite alto (leitura é barata)
- Pagamento e exportação: limite baixo (operação cara)
const limits = {
login: new RedisRateLimiter("login", 5, 60_000), // 5/min
register: new RedisRateLimiter("register", 3, 3_600_000), // 3/hora
products: new RedisRateLimiter("products", 1000, 60_000), // 1000/min
charge: new RedisRateLimiter("charge", 10, 60_000), // 10/min
export: new RedisRateLimiter("export", 3, 3_600_000), // 3/hora
};
// Login e cadastro acontecem ANTES de existir usuário autenticado:
// o identificador é o IP (e, no login, também o e-mail tentado)
app.post(
"/auth/login",
rateLimit(limits.login, (req) => `${req.ip}:${req.body.email}`),
);
app.post("/auth/register", rateLimit(limits.register, (req) => req.ip));
// Rotas autenticadas: o identificador é o usuário
app.get("/api/products", rateLimit(limits.products, (req) => req.user.id));
app.post("/api/payment/charge", rateLimit(limits.charge, (req) => req.user.id));
app.post("/api/export", rateLimit(limits.export, (req) => req.user.id));Dois detalhes que costumam passar batido:
- Cada regra tem seu próprio contador. Se o limite de login e o de produtos usarem a mesma chave no Redis, navegar pelo catálogo gasta tentativas de login.
- Na tela de login não existe
req.user. Limitar por usuário ali não protege nada. O limite precisa ser por IP e por conta alvo.
4️⃣ Limite Por IP: Uma Camada a Mais
Limite por usuário não segura quem nem chegou a fazer login. Um limite geral por IP pega bots e scripts antes de qualquer outra coisa.
// Necessário se a API está atrás de proxy, CDN ou load balancer,
// senão req.ip será sempre o IP do proxy
app.set("trust proxy", 1);
app.use(
rateLimit(new RedisRateLimiter("ip", 1000, 60_000), (req) => req.ip),
);Cuidados com limite por IP:
- Muita gente pode dividir o mesmo IP. Empresas, universidades e operadoras de celular colocam centenas de pessoas atrás de um único endereço. Seja generoso aqui.
trust proxymal configurado é pior que nada. Ou todo mundo vira um IP só, ou o atacante forja o headerX-Forwarded-Fore escapa do limite.
E o Ataque de Verdade?
Uma ressalva honesta: rate limiting na aplicação não segura um ataque distribuído de grande porte. Quando milhares de máquinas atacam ao mesmo tempo, cada requisição ainda chega ao seu servidor, abre conexão e consome recurso, mesmo que seja para responder 429.
Contra isso, a proteção precisa estar antes da sua aplicação: CDN e WAF (Cloudflare, AWS WAF e similares), que absorvem o volume na borda da rede.
O rate limiting da aplicação resolve o resto, que na prática é a maior parte do dia a dia: integração mal feita, bot de scraping, força bruta no login, script em loop.
Trade-offs: Proteção vs Experiência
Limite Muito Baixo
✅ Protege contra abuso ❌ Usuários legítimos ficam frustrados
Limite Muito Alto
✅ Ninguém fica bloqueado ❌ Um único cliente ainda consegue derrubar o sistema
Solução: limites por plano
const limitsByPlan = {
free: 100, // por minuto
pro: 10_000,
enterprise: null, // definido em contrato, por cliente
};
const capacity = limitsByPlan[user.plan] ?? user.customLimit ?? 100;De quebra, o limite vira argumento de venda: quem precisa de mais, paga mais.
Implementação: Uma Tarde
Hora 1:
- Subir o Redis
- Implementar o limitador (ou adotar uma biblioteca pronta)
Hora 2:
- Criar o middleware
- Aplicar nos endpoints críticos: login, cadastro, pagamento, exportação
Hora 3:
- Testar com
ab(Apache Bench) ou k6 - Ajustar os limites com base no teste
Hora 4:
- Alerta quando o volume de respostas 429 subir (veja o post sobre monitoramento)
- Painel com os clientes que mais batem no limite
# Teste de carga rápido
ab -n 1000 -c 100 http://localhost:3000/api/products
# Resultado esperado: parte das requisições respondida com 429Checklist: Você Está Protegido?
- Tem rate limiting implementado?
- Os limites variam por endpoint?
- Tem limite por IP, além do limite por usuário?
- O contador é compartilhado entre servidores (Redis ou similar)?
- As respostas informam o limite restante e o
Retry-After? - Monitora o volume de respostas 429?
- Fez teste de carga?
- Login e cadastro têm limites baixos, por IP e por conta?
- Operações caras (pagamento, exportação) têm limites baixos?
- Tem CDN ou WAF na frente para ataques de grande volume?
Menos de 6 marcados = você está em risco.
O Medo Real: Bloquear Cliente Legítimo
Quando você implementa rate limiting, aparece um medo:
"E se um cliente legítimo bater no limite?"
Vai acontecer. Um cliente integra sua API, o código dele tem um bug que dispara requisições em paralelo, e o limite entra em ação.
E isso é bom. O bug dele afeta só ele, e não todos os seus outros clientes.
O que faz diferença é a comunicação:
{
"error": "Too Many Requests",
"message": "Você atingiu o limite de 100 requisições por minuto.",
"retryAfter": 45,
"documentation": "https://docs.sua-api.com/rate-limiting",
"support": "suporte@sua-api.com"
}Avise, explique, ofereça suporte. E monitore quem bate no limite com frequência: muitas vezes é um cliente que precisa de ajuda na integração, ou de um plano maior.
Próximos Passos
Se você deixou mais de 4 itens do checklist em branco:
Sua API está vulnerável.
Uma tarde implementando rate limiting economiza:
- Horas fora do ar por abuso ou ataque
- Clientes legítimos prejudicados por um único cliente mal comportado
- A investigação caótica de "por que tudo ficou lento?"
Quer Aprofundar?
Se você está escalando seu sistema e quer garantir que nenhuma requisição maliciosa (ou mal planejada) derrube tudo, posso ajudar com:
- Auditoria de Segurança da API: mapeamento de vulnerabilidades e teste de carga
- Rate Limiting Implementado: por usuário, por IP e por endpoint
- Proteção em Camadas: CDN, WAF e limites na aplicação trabalhando juntos
- Monitoramento e Alertas: detectar abuso antes de virar problema
Falar comigo