Como proteger APIs contra DoS usando rate limiting com Redis
Aprenda a blindar suas APIs REST contra tráfego malicioso e ataques de negação de serviço utilizando Redis e o algoritmo de token bucket em ambientes distribuídos.
Resumo
- O algoritmo de token bucket permite rajadas controladas de tráfego sem penalizar usuários legítimos.
- O Redis garante alta velocidade e consistência de dados em arquiteturas distribuídas com múltiplos nós.
- Scripts em Lua executados diretamente no Redis evitam condições de corrida em operações concorrentes.
- Chaves baseadas em IP ou tokens de autenticação isolam abusos de forma granular por cliente.
- Cabeçalhos HTTP padronizados mantêm o cliente informado sobre o consumo e os limites restantes.
O perigo invisível do tráfego descontrolado em APIs REST
Manter uma API REST aberta na internet é um convite constante para problemas se não houver um controle adequado de quem acessa o quê e com que frequência. Imagine que sua aplicação web recém-lançada comece a receber milhares de requisições por segundo vindas de um script malicioso ou de um robô de raspagem de dados. Na prática, isso significa que seus servidores sofrerão uma sobrecarga severa, esgotando conexões de banco de dados e tirando o sistema do ar para usuários legítimos. Proteger seus endpoints críticos exige uma estratégia robusta de controle de taxa, conhecida no mundo da engenharia de software como rate limiting, que funciona como um segurança rigoroso na porta de uma festa concorrida.
Em arquiteturas modernas baseadas em microsserviços, onde dezenas de instâncias de uma mesma API rodam simultaneamente atrás de um balanceador de carga, aplicar esse controle de forma isolada em cada servidor não funciona. Se cada servidor contar apenas o próprio tráfego, um invasor conseguirá multiplicar o limite de requisições pelo número de servidores ativos, driblando a proteção completamente. É justamente nesse cenário distribuído que precisamos de uma fonte central e veloz de verdade, capaz de registrar contadores de acesso em tempo real e de forma atômica para todo o cluster de servidores. Antes de avançarmos para a solução, vale recordar como mitigamos outros gargalos operacionais, a exemplo do que discutimos ao ver como proteger APIs contra força bruta usando Redis e Lua para bloquear tentativas repetidas de login.
Por que o algoritmo de token bucket é ideal para APIs
Existem várias formas matemáticas de controlar o fluxo de requisições, mas o algoritmo de token bucket, ou balde de tokens, destaca-se pela flexibilidade e excelente experiência do usuário. Para entender o conceito na prática, imagine um balde que recebe fichas ou tokens a uma taxa constante, digamos dez tokens por segundo, até atingir uma capacidade máxima de cinquenta tokens. Cada vez que um cliente faz uma requisição HTTP para a sua API, o sistema retira um token do balde e permite o acesso. Se o balde estiver completamente vazio, a requisição é rejeitada imediatamente com um código de erro HTTP 429 Too Many Requests, indicando que o limite foi excedido.
A grande vantagem desse modelo em comparação a um contador simples de janela fixa é que ele tolera rajadas legítimas de tráfego. Se um usuário carregar um painel complexo que dispara cinco requisições simultâneas, ele conseguirá fazê-lo sem problemas, desde que haja fichas acumuladas no balde. Logo após essa rajada, o balde voltará a se encher gradualmente conforme o tempo passa, garantindo um equilíbrio perfeito entre segurança e usabilidade. Para implementar essa lógica de forma eficiente, precisamos de um banco de dados em memória que responda em frações de milissegundo, e é exatamente aí que entra o ecossistema do Redis.
Arquitetura distribuída com Redis e contadores atômicos
O Redis é um armazenamento de dados em memória extremamente rápido, estruturado no formato de chave-valor, que funciona perfeitamente como um cache centralizado para o nosso sistema de controle. Quando múltiplos servidores de API precisam consultar e atualizar o saldo de tokens de um usuário no mesmo milissegundo, surge o desafio da concorrência e das chamadas condições de corrida. Se dois servidores lerem o mesmo valor de tokens ao mesmo tempo, subtraírem um e salvarem de volta, o saldo final ficará incorreto, permitindo mais acessos do que o permitido. Para evitar esse tipo de falha silenciosa, a engenharia de software confia na execução de scripts na linguagem Lua direto dentro do servidor Redis.
Como o Redis executa scripts Lua de forma estritamente atômica, garantimos que nenhuma outra operação consiga interferir no balde de tokens enquanto o cálculo de consumo e reabastecimento estiver acontecendo. No script, calculamos o tempo decorrido desde a última requisição, adicionamos novos tokens proporcionalmente ao tempo passado respeitando o limite máximo do balde, verificamos se há saldo suficiente, subtraímos a unidade gasta e atualizamos o estado — tudo em uma única transação ultra rápida. Na prática, isso significa que mesmo sob uma enxurrada massiva de acessos concorrentes vindos de diferentes regiões geográficas, o cálculo do rate limiter será matematicamente perfeito e livre de inconsistências.
Implementando a lógica com código funcional
Para colocar a mão na massa, vamos analisar um trecho de código em Node.js utilizando a biblioteca ioredis combinada com um script Lua para gerenciar o token bucket. Essa abordagem centralizada garante que qualquer instância da nossa API consiga validar o acesso de forma instantânea, mantendo o consumo de memória sob controle estrito no servidor de cache.
const Redis = require('ioredis');
const redis = new Redis();
const tokenBucketScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'last_updated')
local tokens = tonumber(data[1])
local last_updated = tonumber(data[2])
if not tokens then
tokens = capacity
last_updated = now
else
local elapsed = math.max(0, now - last_updated)
local generated = elapsed * refillRate
tokens = math.min(capacity, tokens + generated)
last_updated = now
end
if tokens < requested then
redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
return 0
end
tokens = tokens - requested
redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
return 1
`;
async function checkRateLimit(clientId) {
const capacity = 50;
const refillRate = 5;
const now = Date.now() / 1000;
const requested = 1;
const result = await redis.eval(tokenBucketScript, 1, clientId, capacity, refillRate, now, requested);
return result === 1;
}O código acima demonstra como o estado do balde é recuperado, recalculado com base no tempo atual do sistema e atualizado de forma atômica no Redis. O uso de hashes do Redis para armazenar o número de tokens e o carimbo de data/hora otimiza o consumo de espaço em memória, permitindo gerenciar milhões de usuários ativos simultaneamente com impacto quase nulo na latência das requisições HTTP da sua API.
Boas práticas operacionais e comunicação com o cliente
Implementar a restrição técnica de acesso é apenas metade do trabalho; a outra metade envolve como a sua aplicação comunica essa restrição aos clientes legítimos e sistemas integrados. Sempre que uma requisição for bloqueada por excesso de tráfego, a API deve retornar imediatamente o código de status HTTP 429, acompanhado de cabeçalhos informativos padronizados. Esses cabeçalhos, conhecidos como RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset, permitem que desenvolvedores terceiros ajustem o ritmo de suas aplicações consumidoras de forma automatizada, evitando bloqueios desnecessários e melhorando a resiliência geral do ecossistema.
Além disso, é fundamental escolher criteriosamente a chave de identificação utilizada para aplicar o limite. Dependendo do modelo de negócio da sua aplicação, você pode limitar o acesso com base no endereço IP do cliente, no token de autenticação JWT, ou em uma chave de API corporativa. Para rotas públicas de login ou recuperação de senha, o IP é indispensável para barrar ataques de força bruta, enquanto rotas autenticadas de alta performance funcionam melhor quando associadas diretamente ao identificador do plano do usuário. Monitorar o comportamento dessas chaves por meio de métricas em tempo real garante que você consiga ajustar os limites operacionais antes que um pico inesperado afete a estabilidade da infraestrutura. Para complementar essa implementação em produção, vale conferir também nossas análises técnicas sobre Como eliminar o Layout Shift no carregamento de fontes com CSS e Como criar microsserviços resilientes com circuit breaker.
Considerações finais sobre resiliência em microsserviços
Blindar APIs REST contra abuso, tráfego malicioso e picos repentinos de carga deixou de ser um diferencial estético e passou a ser um requisito básico de sobrevivência para qualquer aplicação corporativa moderna. Ao combinar a velocidade inigualável do Redis com a flexibilidade matemática do algoritmo de token bucket, criamos uma barreira defensiva robusta e distribuída que protege os recursos computacionais sem penalizar os usuários legítimos. O investimento em uma arquitetura de controle de taxa bem planejada se paga rapidamente na forma de maior estabilidade operacional, redução de custos com servidores provisionados em excesso e uma experiência de usuário consistentemente fluida, independentemente do volume de acessos recebidos.