O que é rate limiting?
Uma API pública ou privada pode receber muitas requisições em poucos segundos. Parte desse tráfego é legítima, como um aplicativo móvel sincronizando dados, mas outra parte pode vir de scripts mal configurados, robôs, tentativas de força bruta ou ataques de negação de serviço. O rate limiting em APIs Node.js é uma técnica que limita quantas solicitações um cliente pode realizar durante um intervalo definido.
Em vez de permitir chamadas ilimitadas, o servidor mantém uma contagem por usuário, chave de API, endereço IP ou outro identificador. Quando o limite é ultrapassado, a API responde normalmente com o código HTTP 429 Too Many Requests. Isso protege recursos, reduz custos e ajuda a manter uma experiência estável para todos os consumidores.
Antes de avançar, vale revisar o que é Node.js, entender o que é uma API e consultar o tutorial sobre como criar uma API com Node.js.
Por que limitar requisições?
Sem controle, um único cliente pode consumir grande parte da CPU, memória, conexões de banco e largura de banda disponíveis. Mesmo quando não existe intenção maliciosa, um laço infinito no front-end pode gerar milhares de chamadas e afetar todos os usuários.
O rate limiting oferece várias vantagens:
- reduz tentativas automatizadas de login;
- impede abuso de endpoints caros;
- evita que um consumidor monopolize a infraestrutura;
- controla custos de APIs externas e serviços em nuvem;
- cria planos de uso diferentes para usuários gratuitos e pagos;
- ajuda a absorver picos inesperados de tráfego.
Ele não substitui autenticação, autorização, cache, validação ou firewall. É uma camada adicional dentro de uma estratégia de segurança e desempenho. Para uma visão mais ampla, veja também como proteger aplicações web contra ataques comuns e como otimizar APIs RESTful em Node.js.
Preparando uma API Express
Crie um projeto simples e instale o Express e o middleware express-rate-limit:
mkdir api-rate-limit
cd api-rate-limit
npm init -y
npm install express express-rate-limitCrie o arquivo server.js:
const express = require('express');
const { rateLimit } = require('express-rate-limit');
const app = express();
app.use(express.json());
app.get('/status', (req, res) => {
res.json({ ok: true, time: new Date().toISOString() });
});
app.listen(3000, () => {
console.log('API disponível em http://localhost:3000');
});Execute com node server.js. Neste momento, o endpoint ainda aceita qualquer quantidade de chamadas.
Configurando um limite global
Adicione um limitador antes das rotas:
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 100,
standardHeaders: 'draft-7',
legacyHeaders: false,
message: {
error: 'Muitas requisições. Tente novamente mais tarde.'
}
});
app.use(globalLimiter);A configuração permite até 100 requisições por cliente a cada 15 minutos. O middleware identifica o cliente, incrementa a contagem e adiciona cabeçalhos que informam o limite e quanto ainda resta.
A documentação oficial do express-rate-limit explica todas as opções suportadas. Também é útil consultar a definição do código HTTP 429 no MDN.
Aplicando limites diferentes por rota
Nem todos os endpoints têm o mesmo risco. Uma rota de status pode aceitar mais chamadas, enquanto login, recuperação de senha e geração de relatórios devem ter regras mais rígidas.
const loginLimiter = rateLimit({
windowMs: 10 * 60 * 1000,
limit: 5,
skipSuccessfulRequests: true,
message: {
error: 'Muitas tentativas de login.'
}
});
app.post('/login', loginLimiter, async (req, res) => {
const { email, password } = req.body;
// Exemplo simplificado de autenticação
if (email !== 'teste@exemplo.com' || password !== 'segredo') {
return res.status(401).json({ error: 'Credenciais inválidas' });
}
res.json({ authenticated: true });
});Com skipSuccessfulRequests, tentativas bem-sucedidas não permanecem na contagem. Isso é útil contra força bruta, mas deve ser adotado com cuidado: a autenticação precisa ser confiável e rápida.
Escolhendo a chave de identificação
O padrão costuma usar o endereço IP. Isso é simples, mas pode gerar falsos positivos em empresas, escolas ou redes móveis nas quais muitos usuários compartilham o mesmo IP. Quando a API possui autenticação, uma chave baseada no usuário pode ser mais justa.
const userLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 30,
keyGenerator: (req) => {
return req.user?.id || req.ip;
}
});Essa abordagem exige que o middleware de autenticação seja executado antes do limitador. Nunca aceite diretamente um identificador enviado em um cabeçalho sem verificar sua autenticidade, pois o cliente poderia alterá-lo para escapar da regra.
Proxy reverso e endereço IP real
Em produção, a aplicação frequentemente fica atrás de Nginx, Cloudflare, balanceador ou plataforma em nuvem. Nesse cenário, o Express pode enxergar apenas o IP do proxy e tratar todos os usuários como se fossem um único cliente.
Configure trust proxy de acordo com a infraestrutura:
app.set('trust proxy', 1);O valor correto depende da quantidade e do tipo de proxies. Uma configuração ampla demais pode permitir falsificação de IP. Verifique a documentação do Express e teste req.ip no ambiente real antes de liberar a API.
Armazenamento distribuído com Redis
O armazenamento em memória funciona em um único processo. Porém, quando a aplicação usa múltiplas instâncias, cada servidor mantém sua própria contagem. Um cliente poderia fazer chamadas alternadas e multiplicar o limite disponível.
A solução é usar um armazenamento compartilhado, normalmente Redis. Todas as instâncias consultam a mesma contagem, mantendo a regra consistente. Além disso, o Redis suporta expiração automática e operações atômicas.
Em uma arquitetura escalável, combine o middleware com um store compatível com Redis. Defina prefixos diferentes para cada aplicação e monitore indisponibilidade. Se o Redis falhar, escolha conscientemente entre bloquear requisições ou permitir temporariamente o tráfego. Essa decisão depende do risco do endpoint.
Algoritmos de rate limiting
Existem diferentes estratégias para contabilizar chamadas:
- Janela fixa: reinicia a contagem em intervalos definidos. É simples, mas permite picos na transição entre janelas.
- Janela deslizante: considera continuamente o período anterior e distribui melhor as solicitações.
- Token bucket: adiciona tokens ao longo do tempo e permite pequenos picos controlados.
- Leaky bucket: processa solicitações em ritmo estável, funcionando como uma fila com vazão limitada.
A escolha depende da experiência desejada. Para endpoints comuns, uma janela simples pode bastar. Para APIs comerciais com alto volume, token bucket ou janela deslizante oferecem controle mais preciso.
Retornando respostas úteis ao cliente
Uma resposta 429 deve ser clara e previsível. Evite retornar páginas HTML quando a API trabalha com JSON.
{
"error": "rate_limit_exceeded",
"message": "Limite de requisições atingido.",
"retryAfterSeconds": 60
}Quando possível, envie o cabeçalho Retry-After. O cliente pode aguardar antes de tentar novamente e aplicar exponential backoff, aumentando gradualmente o intervalo entre tentativas.
Não revele detalhes internos desnecessários, como regras de infraestrutura ou informações de outros usuários. O suficiente é informar que o limite foi atingido e quando uma nova tentativa poderá ser feita.
Planos e limites por usuário
Produtos SaaS frequentemente possuem limites diferentes por plano. Usuários gratuitos podem ter 100 chamadas por hora, enquanto clientes empresariais recebem milhares.
Carregue o plano após autenticar o usuário e escolha a configuração correspondente. Evite consultar o banco em toda requisição apenas para descobrir o limite; use cache com expiração e invalide quando o plano mudar.
Também separe limites por operação. Uma consulta simples pode custar pouco, enquanto exportar um relatório ou processar inteligência artificial consome muito mais recursos. Nesses casos, uma métrica baseada em créditos pode ser melhor do que apenas contar chamadas.
Monitoramento e observabilidade
Registre eventos de bloqueio sem salvar dados sensíveis. Métricas importantes incluem:
- quantidade de respostas 429;
- endpoints que mais atingem limites;
- usuários ou chaves com maior consumo;
- latência do armazenamento de contagem;
- tentativas de login bloqueadas;
- variações de tráfego por horário.
Um aumento repentino pode indicar ataque, integração quebrada ou limite mal dimensionado. Crie alertas, mas evite registrar cada requisição em excesso, pois isso pode aumentar custos e gerar ruído.
Como testar o limitador
Teste manualmente com um laço:
for i in {1..10}; do
curl -i http://localhost:3000/status
doneEm testes automatizados, reduza temporariamente a janela e o limite. Verifique chamadas permitidas, resposta 429, cabeçalhos e liberação após expiração. Limpe o store entre testes para evitar interferência.
Também simule múltiplos usuários, proxies e instâncias. O guia de testes unitários com Jest apresenta fundamentos úteis para estruturar essa validação.
Erros comuns
- Usar somente IP: pode bloquear redes compartilhadas.
- Confiar em cabeçalhos do cliente: permite trocar o identificador.
- Esquecer múltiplas instâncias: o limite fica multiplicado.
- Não configurar proxy: todos podem aparecer com o mesmo IP.
- Aplicar uma regra única: endpoints sensíveis exigem limites menores.
- Ignorar resposta 429: clientes continuam repetindo chamadas.
- Tratar rate limiting como segurança completa: autenticação e validação continuam obrigatórias.
Conclusão
Implementar rate limiting em APIs Node.js é uma medida simples que produz ganhos relevantes de segurança, estabilidade e controle de custos. Comece com um limite global conservador, aplique regras específicas em login e operações caras e acompanhe métricas antes de ajustar os números.
Em ambientes distribuídos, use um armazenamento compartilhado como Redis, configure corretamente o proxy e identifique clientes autenticados sempre que possível. Combine a técnica com cache, validação, monitoramento e testes. Assim, sua API fica mais resistente a abusos sem prejudicar usuários legítimos.




