Marcio Cunha

Por que o rate limiting falha em microsserviços?

Descubra por que sistemas de controle de requisições baseados em Redis falham sob concorrência extrema e como evitar falsos positivos.

Marcio Cunha•6 min
Também disponível em:EspañolEnglish
Resumo
  • Operações de incremento atômico em clusters distribuídos sofrem com latência de rede e bloqueios de hash slots.
  • Condições de corrida durante picos de tráfego geram contadores imprecisos e bloqueiam usuários legítimos indevidamente.
  • Scripts Lua garantem atomicidade em instâncias individuais, mas perdem eficácia quando chaves estão espalhadas.
  • Estratégias de amostragem local combinadas com sincronização assíncrona reduzem drasticamente a pressão sobre o cache.
  • Arquiteturas híbridas combinando limites na borda com contadores locais equilibram precisão e resiliência.

O desafio invisível do controle de tráfego distribuído

Quando construímos sistemas modernos baseados em microsserviços, um dos maiores quebra-cabeças é proteger nossas APIs contra abusos, picos repentinos de acesso ou ataques de negação de serviço. Para resolver isso, costumamos implementar mecanismos de limitação de taxa, conhecidos no mercado como rate limiting. A ideia central parece simples: contar quantas vezes um determinado usuário ou IP fez uma requisição dentro de uma janela de tempo específica e rejeitar o pedido caso o limite seja ultrapassado. Na prática, porém, esse processo esbarra em barreiras físicas complexas quando a aplicação cresce e se espalha por múltiplos servidores.

Em arquiteturas monolíticas tradicionais, manter esse contador é uma tarefa trivial mantida diretamente na memória RAM ou em uma única base de dados relacional. Contudo, quando fragmentamos o sistema em dezenas de microsserviços independentes que precisam escalar horizontalmente — ou seja, adicionando novas máquinas conforme a demanda cresce —, o estado compartilhado deixa de existir de forma centralizada. É justamente nesse cenário de alta concorrência que muitas equipes recorrem a tecnologias de cache em memória de alta performance, como o Redis, para centralizar o controle das contagens em um único ponto acessível por todos os serviços da malha.

O problema é que assumir que uma ferramenta rápida como o Redis resolve magicamente todos os problemas de sincronização é um erro arquitetural comum. Na prática, isso significa que, quando milhares de requisições chegam exatamente no mesmo milissegundo, a infraestrutura de dados começa a sofrer com gargalos de rede, contenção de CPU e problemas de consistência eventual. Para entender melhor os impactos disso, vale a pena relembrar conceitos fundamentais que discutimos sobre fluxos baseados em tokens, onde a velocidade de reabastecimento do saldo precisa acompanhar o ritmo frenético das requisições de clientes impacientes.

Como o Redis Cluster divide os dados e onde o modelo falha

Para escalar além dos limites de uma única máquina física, a comunidade costuma adotar o Redis Cluster, que distribui automaticamente os dados entre múltiplos nós usando um conceito chamado hash slots. O espaço total de chaves é dividido em 16384 partições lógicas, e cada nó do cluster fica responsável por um subconjunto dessas partições. Quando uma aplicação envia um comando para atualizar um contador de rate limiting, o cliente Redis calcula em qual slot aquela chave específica se encaixa e direciona a operação diretamente para o servidor responsável por ela.

Na teoria, essa distribuição inteligente evita que um único servidor vire o gargalo de todo o ecossistema. No entanto, em cenários de tráfego intenso — como uma campanha de vendas relâmpago ou uma Black Friday —, milhões de usuários podem tentar acessar o mesmo recurso crítico simultaneamente, gerando o que chamamos de hotspotting. Quando muitas requisições apontam para chaves que caem no mesmo hash slot ou no mesmo nó físico, a capacidade de processamento daquele servidor específico esgota-se rapidamente, elevando a latência de resposta a níveis inaceitáveis e provocando timeouts em cadeia.

Além disso, o modelo de replicação assíncrona padrão do Redis introduz uma janela perigosa de vulnerabilidade. Se o nó principal de um cluster recebe atualizações frenéticas de contadores e sofre uma pane antes de conseguir replicar esses dados para os nós secundários, a liderança é transferida para uma réplica que pode estar com o contador desatualizado. Na prática, isso significa que usuários que já haviam estourado sua cota de requisições ganham uma segunda chance imerecida, burlando completamente as regras de segurança e sobrecarregando ainda mais os microsserviços de backend.

Condições de corrida e a ilusão da atomicidade simples

Um erro clássico de implementação ocorre quando os desenvolvedores confiam em comandos isolados do Redis, como a sequência de leitura e escrita envolvendo get e set para atualizar um contador. Em um ambiente concorrente, duas instâncias de microsserviços podem ler o mesmo valor do contador ao mesmo tempo, somar uma unidade localmente e reescrever o resultado no cache. Essa falha conceitual, conhecida na engenharia de software como condição de corrida, faz com que múltiplos acessos simultâneos sejam contabilizados como se fossem apenas um, permitindo que o cliente envie muito mais requisições do que o permitido.

Para mitigar esse comportamento indesejado, costuma-se utilizar scripts em linguagem Lua executados diretamente no lado do servidor Redis, garantindo que a operação de leitura, verificação e incremento ocorra de forma atômica e sem interrupções. Embora os scripts Lua resolvam o problema da concorrência dentro de um único nó, eles não eliminam o atrito gerado quando múltiplos clientes disputam o mesmo recurso de rede. Sob carga extrema, a fila de execução desses scripts dentro do Redis cresce vertiginosamente, fazendo com que as conexões fiquem travadas à espera de liberação.

O resultado direto desse represamento de conexões é o surgimento de falsos positivos indesejados. Clientes legítimos que tentam interagir com a aplicação começam a receber respostas de erro por estarem supostamente excedendo os limites, quando na verdade o sistema de controle de taxa é que está congestionado e incapaz de processar as validações em tempo hábil. É um cenário irônico onde a ferramenta criada para proteger o sistema acaba se tornando a principal causa de indisponibilidade para os usuários finais durante os picos de acesso.

Estratégias práticas para evitar falsos positivos sob concorrência

Para contornar os gargalos do Redis Cluster e eliminar os falsos positivos, a engenharia moderna tem adotado abordagens híbridas que reduzem drasticamente a dependência de chamadas síncronas centralizadas. Uma das técnicas mais eficazes é a implementação de cache local em memória na camada de borda ou no próprio microsserviço, utilizando algoritmos como o Token Bucket com sincronização em lote. Em vez de consultar o Redis a cada clique do usuário, o microsserviço mantém um saldo local aproximado e realiza sincronizações assíncronas em segundo plano a cada poucos segundos.

Outra estratégia robusta consiste em particionar as chaves de controle utilizando saltos aleatórios ou sufixos baseados no tempo, espalhando artificialmente o volume de gravações por diferentes hash slots do cluster. Por exemplo, em vez de armazenar o contador de um IP em uma única chave estática, podemos dividi-lo em quatro chaves diferentes baseadas em frações de tempo ou sub-redes, somando os valores apenas no momento da validação. Isso distribui a carga de escrita uniformemente entre todos os nós do cluster, evitando os temidos hotspots de CPU.

Abaixo, apresentamos um exemplo conceitual de script em Lua utilizado para gerenciar limites de taxa com controle de janela deslizante de forma atômica no Redis, minimizando inconsistências sob concorrência:

local key = KEYS[1]local now = tonumber(ARGV[1])local window = tonumber(ARGV[2])local limit = tonumber(ARGV[3])redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)local current_requests = redis.call('ZCARD', key)if current_requests < limit then    redis.call('ZADD', key, now, now)    redis.call('PEXPIRE', key, window)    return 1else    return 0end

Esse script utiliza conjuntos ordenados do Redis para registrar cada requisição com seu respectivo timestamp, limpando automaticamente registros antigos fora da janela de tempo e avaliando se o volume atual está dentro do teto permitido. Para aprofundar ainda mais a resiliência da sua infraestrutura contra falhas sistêmicas em momentos de pico, vale a pena estudar técnicas avançadas de sincronização de estado com scripts Lua, que ajudam a isolar componentes defeituosos antes que derrubem toda a aplicação.

Considerações Finais

O controle de tráfego em microsserviços distribuídos exige muito mais do que simplesmente aplicar uma biblioteca de rate limiting sobre uma instância de cache compartilhada. Como vimos, o Redis Cluster, apesar de extremamente veloz, introduz desafios severos de hotspots, condições de corrida e gargalos de rede sob concorrência extrema que podem sabotar a experiência do usuário com falsos positivos.

Adotar arquiteturas híbridas com contadores locais, particionamento inteligente de chaves e sincronização assíncrona é o caminho mais seguro para garantir estabilidade e precisão. Ao compreender os trade-offs operacionais e projetar defesas resilientes, sua engenharia consegue absorver picos de tráfego avassaladores sem comprometer a confiabilidade dos serviços essenciais.