Como blindar APIs REST contra abuso de requisições com Redis
Descubra como proteger microsserviços contra sobrecarga e ataques de negação de serviço utilizando rate limiting distribuído com Redis e o algoritmo token bucket.
Resumo
- O algoritmo token bucket permite rajadas controladas de tráfego sem penalizar usuários legítimos.
- O Redis centraliza o controle de contadores em memória com alta velocidade e suporte a operações atômicas.
- Microsserviços distribuídos precisam de sincronização eficiente para evitar falsos positivos no bloqueio.
- Scripts em Lua garantem que checagem e atualização do balde ocorram sem concorrência indevida.
- Respostas HTTP padronizadas com cabeçalhos de limite ajudam clientes a gerenciarem suas próprias filas.
Por que APIs modernas sofrem com sobrecarga e abuso?
Manter uma aplicação web no ar exige resiliência contra picos inesperados de tráfego, scripts maliciosos e clientes que realizam dezenas de chamadas por segundo. Na prática, isso significa que um único robô de varredura ou uma integração mal programada pode esgotar os recursos de CPU e memória dos seus servidores de backend. Para mitigar esse risco de forma eficiente, a engenharia de software recorre ao rate limiting, que consiste em impor limites claros na quantidade de requisições que cada usuário ou endereço IP pode efetuar em um intervalo de tempo específico. Se você deseja aprofundar suas defesas de infraestrutura, vale a pena entender como proteger APIs contra DoS usando rate limiting com Redis para garantir estabilidade operacional sob pressão.
Quando operamos em arquiteturas de microsserviços, o desafio se torna ainda mais complexo porque as requisições chegam pulverizadas por múltiplos gateways de API e nós de processamento. Se cada instância decidir contar o número de acessos de forma isolada em sua própria memória RAM, um cliente mal intencionado conseguirá multiplicar o limite de requisições pelo número de servidores ativos. Para resolver essa falha estrutural, precisamos de uma ferramenta centralizada de alta performance que armazene os contadores de forma compartilhada e instantânea. É justamente nesse cenário que entra o ecossistema distribuído, exigindo uma solução coesa e tolerante a falhas antes mesmo de precisarmos diagnosticar falhas profundas com ferramentas como microsserviços travando e como rastrear falhas com OpenTelemetry em ambientes de produção.
Como funciona o algoritmo token bucket na prática?
Existem várias formas matemáticas de controlar o fluxo de dados, mas o algoritmo conhecido como token bucket, ou balde de tokens, destaca-se pela flexibilidade e elegância. Imagine um balde virtual que armazena fichas ou tokens até um limite máximo preestabelecido. Esse balde é reabastecido constantemente com novos tokens a uma taxa fixa por segundo, por exemplo, dez fichas novas a cada dez segundos. Cada vez que uma API recebe uma requisição HTTP, ela tenta retirar um token do balde do cliente correspondente. Se houver tokens disponíveis, a requisição segue adiante para processamento e o balde perde um token; se o balde estiver completamente vazio, a chamada é recusada imediatamente com o código de status HTTP 429 Too Many Requests.
A grande vantagem dessa abordagem em comparação a contadores simples de janela fixa é a permissão natural para rajadas de tráfego legítimas, conhecidas no mercado como burst. Se um usuário estiver navegando normalmente e de repente carregar uma página cheia de componentes que disparam várias chamadas simultâneas, ele consome os tokens acumulados no balde de uma só vez. Desde que o volume total não ultrapasse a capacidade máxima, a operação é bem-sucedida. Logo em seguida, o sistema repõe os tokens consumidos gradualmente conforme o tempo passa. Isso evita aquela frustração comum em que o usuário é bloqueado injustamente só porque executou ações rápidas e legítimas no painel da aplicação.
Implementando o controle distribuído com Redis
Para colocar essa lógica em produção em um ambiente com múltiplos servidores, precisamos de um banco de dados em memória extremamente rápido e confiável, sendo o Redis a escolha padrão da indústria. O Redis armazena pares de chave e valor na memória principal, permitindo consultas e atualizações em frações de milissegundo. No entanto, simplesmente ler o número de tokens, fazer uma conta matemática na linguagem da nossa API e salvar de volta pode gerar um problema clássico de concorrência chamado condição de corrida. Se duas requisições chegarem exatamente no mesmo milissegundo, ambas podem ler o mesmo saldo antigo e permitir o acesso indevido, ultrapassando o limite estipulado.
Para eliminar qualquer brecha de concorrência sem prejudicar a performance geral do sistema, utilizamos scripts escritos na linguagem Lua executados diretamente dentro do Redis. O mecanismo de execução do Redis garante que um script em Lua rode de forma atômica, o que significa que nenhuma outra operação consegue modificar aqueles dados enquanto o script estiver rodando. O script calcula quantos tokens devem ser adicionados desde a última verificação com base no tempo decorrido, verifica se há saldo suficiente, desconta o token se possível e devolve o novo estado atualizado. Desta forma, mesmo que centenas de microsserviços consultem o mesmo Redis simultaneamente, a contabilidade permanece absolutamente precisa e incorruptível.
Estruturando o código do filtro de rate limiting
Abaixo apresentamos um exemplo funcional em Node.js utilizando o cliente Redis para aplicar a verificação do balde de tokens antes de liberar a rota da API. O script em Lua encapsula toda a lógica matemática e de persistência para garantir atomicidade e máxima velocidade de resposta em cada requisição recebida.
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 bucket = redis.call('HMGET', key, 'tokens', 'last_updated')local tokens = tonumber(bucket[1])local last_updated = tonumber(bucket[2])if not tokens then tokens = capacitylast_updated = nowelse local elapsed = math.max(0, now - last_updated) tokens = math.min(capacity, tokens + (elapsed * refillRate))endlocal allowed = 0if tokens >= requested then tokens = tokens - requested allowed = 1endredis.call('HMSET', key, 'tokens', tokens, 'last_updated', now)redis.call('EXPIRE', key, math.ceil(capacity / refillRate))return {allowed, tokens}`;async function checkRateLimit(identifier, capacity, refillRate, requested) { const now = Math.floor(Date.now() / 1000); const result = await redis.eval(tokenBucketScript, 1, `rate_limit:${identifier}`, capacity, refillRate, now, requested); return { allowed: result[0] === 1, remainingTokens: result[1] };}Integrar esse bloco de código no middleware da sua aplicação web permite interceptar qualquer rota crítica de forma transparente. Quando a função retorna que a requisição não é permitida, o servidor interrompe o fluxo normal e retorna um cabeçalho informativo ao cliente. É recomendável enviar cabeçalhos HTTP como X-RateLimit-Limit, X-RateLimit-Remaining e Retry-After para que o cliente saiba exatamente quando poderá realizar novas tentativas sem gerar frustração na experiência do usuário final.
Boas práticas operacionais e considerações de resiliência
Adotar rate limiting distribuído com Redis exige atenção a alguns cuidados de infraestrutura para evitar que o próprio mecanismo de segurança se torne um ponto único de falha no sistema. Se o cluster do Redis sofrer uma queda repentina, sua API inteira não deve parar de funcionar simplesmente porque não conseguiu validar os tokens. A prática recomendada de engenharia consiste em implementar um mecanismo de fallback ou tolerância a falhas no código do middleware. Caso ocorra um erro de conexão com o Redis, a aplicação deve registrar o alerta em seus logs e permitir temporariamente a passagem da requisição, priorizando a disponibilidade do serviço em detrimento do controle restrito de tráfego.
Outro ponto crítico de design envolve a escolha correta da chave utilizada para identificar o cliente no Redis. Utilizar apenas o endereço IP da máquina solicitante pode causar bloqueios injustos em redes corporativas, escritórios ou universidades onde centenas de pessoas compartilham o mesmo IP público de saída para a internet. Para evitar esse tipo de colisão operacional, o ideal é combinar o endereço IP com identificadores contextuais adicionais, como o token de autenticação do usuário logado ou a chave de API fornecida no cabeçalho da requisição. Dessa forma, o controle de abuso torna-se cirúrgico, atingindo apenas o comportamento individual e anômalo sem prejudicar usuários legítimos que compartilham a mesma infraestrutura de rede. Para aprofundar esses conceitos na prática em produção, confira também nossa análise técnica sobre Por que o seu app React trava? Como usar o React Profiler.
Considerações Finais
Blindar APIs REST contra abuso de requisições deixou de ser um luxo operacional e passou a ser um requisito básico de sobrevivência para qualquer arquitetura moderna de microsserviços. A combinação do algoritmo token bucket com a velocidade e atomicidade do Redis oferece um equilíbrio perfeito entre flexibilidade para rajadas de tráfego legítimas e proteção rigorosa contra sobrecargas maliciosas. Ao implementar scripts em Lua e garantir estratégias inteligentes de chaveamento e fallback, sua engenharia constrói um ecossistema robusto, capaz de absorver impactos e manter a estabilidade operacional mesmo sob condições extremas de uso no mundo real.
Investir tempo no planejamento dessas camadas de proteção evita indisponibilidades custosas e melhora significativamente a reputação e a confiabilidade dos seus serviços perante os clientes. Lembre-se de monitorar continuamente as métricas de bloqueio e ajustar os limites do balde de acordo com o comportamento real do seu público, garantindo uma evolução constante e segura da sua infraestrutura tecnológica.