Descarte Primeiro as Requisições Certas: Load Shedding Baseado em Prioridade sob Sobrecarga
Limites estáticos de RPS descartam o tráfego errado. Concorrência é o que satura um serviço, não a taxa de requisições. A partir das minhas anotações após ler o artigo do InfoQ sobre proteção contra sobrecarga, o post de janeiro da Uber sobre o Cinnamon e a palestra da Netflix no QCon SF sobre load shedding priorizado em nível de serviço, eis por que latência é o sinal de controle correto — e como uma pequena taxonomia de prioridades aliada a um limite adaptativo de concorrência mantém o tráfego mais barato sendo descartado primeiro.
Autoscaling e retries ganham as manchetes. Nenhum dos dois ajuda nos dez segundos entre um pico de tráfego atingir um serviço e a próxima instância subir. Nesse intervalo, a taxa de chegada já passou da capacidade do serviço, a fila está enchendo, o p99 está subindo, e toda requisição lá dentro está a caminho de um timeout. A única alavanca que faz algo útil naquele momento é o controle de admissão — o serviço decidindo, em linha, quais requisições aceitar e quais rejeitar antes que cheguem ao caminho lento.
Voltei às minhas anotações sobre isso depois de ler o artigo do InfoQ argumentando que proteção contra sobrecarga é "o pilar faltante da engenharia de plataforma", o post de janeiro de 2026 da Uber sobre o Cinnamon e a palestra do QCon SF 2025 da Netflix sobre load shedding priorizado em nível de serviço. Os três convergem para a mesma arquitetura: um limite adaptativo de concorrência guiado por latência, mais uma taxonomia de prioridades que decide quem é descartado primeiro. O rate limit estático com o qual a maioria dos serviços ainda é entregue não faz nenhum dos dois, e é por isso que descarta o tráfego errado.
A ideia que quero deixar clara: dê a cada requisição uma prioridade e descarte de baixo para cima sob sobrecarga usando um limite adaptativo, para que a saturação degrade o tráfego mais barato em vez de falhar para todos uniformemente.
Por que rate limits estáticos descartam o tráfego errado
A defesa de sobrecarga dos livros-texto é um token bucket — digamos, 5.000 RPS por serviço, aplicado na borda. É fácil de raciocinar e fácil de monitorar em dashboards. Também está errado de duas formas específicas.
A primeira é que RPS não é a variável que quebra um serviço. Concorrência é. A Lei de Little escreve isso precisamente: concorrência média é igual a RPS médio multiplicado pela latência média. Um serviço que processa 5.000 RPS a 10ms mantém 50 requisições em voo. O mesmo serviço mantendo 50 requisições em voo a 100ms está fazendo apenas 500 RPS. O limite de RPS não mudou, mas todo outro recurso — conexões, threads, sockets, conexões downstream — está fazendo dez vezes o trabalho. Um limite estático de RPS escolhe o teto em um ponto de operação e mente sobre todos os outros.
A segunda é que requisições de forma semelhante impõem custos enormemente diferentes. O post da Uber sobre o Docstore — seu armazenamento distribuído chave-valor baseado em MySQL servindo 170M de usuários ativos mensais — é direto sobre isso: requisições de tamanho similar impõem custos variáveis de CPU, memória ou I/O, então um rate limiter sem estado pode descartar tráfego saudável enquanto partições sobrecarregadas permanecem desprotegidas. Uma escrita de 1KB contra uma partição quente não é a mesma carga que uma escrita de 1KB contra uma fria. O token bucket não consegue ver a diferença.
Quando o teto está errado, o modo de falha é uniforme. Todo chamador — a requisição ao vivo que mantém a tela inicial renderizando, a reconciliação em batch que ninguém está observando — recebe 429s na mesma taxa. A chamada voltada ao usuário que importava acabou de ser descartada com a mesma probabilidade do cron job que poderia ter rodado amanhã.
Concorrência adaptativa: latência como sinal de controle
A biblioteca concurrency-limits da Netflix enquadra isso como o mesmo problema de controle que o TCP resolve. Um receptor não sabe de antemão quantos dados em voo a rede consegue suportar. Ele mede: o round-trip time mínimo quando o caminho está vazio, o round-trip time atual sob carga, e a razão entre eles. Quando o RTT atual sobe acima do piso, uma fila está se formando em algum lugar, e o emissor recua. Quando a razão volta a um, o emissor reabre.
Traduza isso para um serviço. Amostre a latência p99 sobre uma janela curta. Compare-a com uma latência mínima suavizada sobre uma janela mais longa. A diferença é um proxy para a profundidade da fila. Se a diferença for pequena, suba o teto de concorrência em um. Se a diferença for grande, derrube o teto agressivamente. Rejeite requisições que cheguem enquanto estiver no teto.
A implementação Vegas da Netflix estima o tamanho da fila com L * (1 - minRTT / sampleRTT), onde L é o limite atual. O teto aumenta em um quando a fila estimada está abaixo de alpha (tipicamente 2 a 3 requisições) e diminui em um quando cruza beta (tipicamente 4 a 6). O Gradient2 generaliza isso rastreando a divergência entre médias exponenciais de janela curta e longa, o que suaviza o impacto de outliers em tráfego irregular. As constantes exatas importam menos do que a propriedade: o teto se move sozinho quando algo downstream desacelera, sem nenhum operador mudar uma config.
Essa é a alavanca que o rate limit estático não pode ser. Um token bucket configurado no RPS de regime estacionário mantém aquele número através de toda mudança de regime — um cold-start de cache, um vizinho barulhento, um downstream lento — e deixa o tráfego em voo se acumular até que threads ou sockets se esgotem. O limite adaptativo colapsa à primeira vista de crescimento de latência e reabre quando ele desaparece. A parte importante é o tempo: isso acontece nos segundos antes que o autoscaling tenha chance de reagir.
Há uma forma esclarecedora de declarar o objetivo. A palestra da Netflix no QCon introduziu dois buffers que achei úteis ao argumentar sobre capacidade: o Success Buffer é a folga acima da baseline que um serviço consegue absorver sem degradação do p99; o Failure Buffer é a capacidade reservada especificamente para rejeitar o excesso graciosamente. A concorrência adaptativa existe para manter o serviço dentro do Success Buffer até que precise recorrer ao Failure Buffer, e o load shedding existe para impedir que o Failure Buffer colapse numa espiral da morte.
A imagem que mantenho no meu caderno para isso é uma curva de latência-versus-carga com dois traços passando o joelho: um para descarte uniforme, onde o p99 de toda classe de prioridade sobe junto; um para descarte priorizado, onde t0 permanece dentro de seu orçamento de latência enquanto t4 e t5 são rejeitados. Passado o joelho, as duas curvas divergem dramaticamente — e essa divergência é toda a razão de fazer esse trabalho.
Atribuindo prioridade sem um comitê
Uma vez que o teto seja honesto sobre quantas requisições concorrentes um serviço consegue rodar, a questão passa a ser quais requisições vivem dentro dele. A resposta menos útil é "toda requisição igualmente, FIFO". Isso é o que o CoDel faz na camada de rede, e é o que a primeira iteração da Uber de proteção contra sobrecarga fez na frente do Docstore. O post do InfoQ sobre o Cinnamon nomeia o modo de falha diretamente: o CoDel descartava tráfego de baixa prioridade e voltado ao usuário indistintamente, o que aumentou a carga de on-call e empurrou retries dos clientes direto de volta para a fila sobrecarregada.
A correção é uma taxonomia de prioridades anexada à própria requisição. A Uber se acomodou em seis camadas, t0 até t5, com t0 reservado para as operações voltadas ao usuário mais sensíveis a latência. O load shedding por serviço da Netflix usa uma ideia similar — o descarte de tráfego não crítico começa em 60% de utilização de CPU, o descarte crítico só entra em ação a 80%. Em ambos os esquemas o teto é o mesmo; o que muda é a ordem em que requisições são admitidas nele. Quando o sistema está saudável tudo entra. Quando a pressão aumenta, t5 e t4 são rejeitados primeiro, depois t3, depois t2. t0 mantém seu orçamento de latência intacto enquanto houver qualquer capacidade para dar a ele.
Duas partes dessa taxonomia importam mais do que o número de camadas.
A primeira é que a prioridade precisa viajar com a requisição, propagada de ponta a ponta. Uma requisição voltada ao usuário que se ramifica em cinco chamadas downstream precisa que cada salto saiba que é t0; caso contrário, o backend mais lento fará decisões de admissão de olhos vendados. Frameworks RPC carregam a tag em metadados ou contexto. A disciplina é fazer isso em toda borda, incluindo as que eu preferiria não tocar.
A segunda é que a taxonomia deve ser pequena o suficiente para se manter correta. Seis camadas é o limite superior que vi alguém defender. A automação da Netflix em torno disso é a parte que a maioria dos adotantes subestima: "a configuração envolve agregar métricas de utilização, e uma função de load shedding por cluster é gerada automaticamente." Sem essa automação, uma atribuição de camada é um comentário em um wiki, divergindo da realidade no momento em que um novo endpoint é entregue.
A menor versão disso por onde eu começaria são duas camadas — interativa e em segundo plano — aplicada serviço por serviço. Isso é o suficiente para recuperar a propriedade que o rate limit estático perdeu: quando o teto encolhe, o trabalho de fundo vai primeiro.
Onde o descarte priorizado sai pela culatra
O padrão não é grátis, e alguns dos modos de falha valem ser nomeados antes de adotá-lo.
O mais comum é classificação incorreta. Todo dono de endpoint acredita que seu endpoint é crítico. Se deixado para declarar sua própria camada sem revisão, um serviço acaba com tudo em t0 e a taxonomia perde sentido. A plataforma automatizada da Netflix existe em parte para prevenir esse desvio: a prioridade é atribuída centralmente contra sinais de negócio, e validada continuamente contra padrões de utilização. Sem esse loop, o trabalho de manter a taxonomia se deteriora a nada em um trimestre.
O segundo é a inanição de tráfego de baixa prioridade sob sobrecarga sustentada. Se o sistema passa uma hora acima do joelho, t4 e t5 são rejeitados por uma hora. Isso pode estar correto — a alternativa é falhar t0 — mas efeitos downstream importam. Jobs em batch acumulam backlogs. A reconciliação atrasa. Um backlog que o sistema pretendia limpar durante uma noite tranquila pode crescer grande o suficiente para precisar do seu próprio incidente. A mitigação é estabelecer pisos explícitos: mesmo sob carga pesada, reserve uma pequena porcentagem do teto para tráfego de baixa prioridade para que o backlog drene a alguma taxa. A biblioteca da Netflix expressa isso diretamente com porcentagens de partição — tráfego ao vivo garantido em 90% da capacidade, batch garantido em 10%, nenhum dos dois passando fome quando ambos estão presentes.
O terceiro é a tempestade de retries. Descartar uma requisição devolve controle ao chamador, e a política típica do chamador é tentar novamente. Se toda requisição descartada for retentada imediatamente, a fila de admissão se reabastece dos clientes mais rápido do que o servidor consegue drená-la, e as mesmas chamadas continuam chegando com maior amplificação. A solução da Netflix é interromper todos os retries quando o descarte do lado do servidor está ativo e só permitir retries de alta prioridade sob carga pesada. O orçamento de retries — uma contagem de retries permitidos por unidade de tempo, expressa como uma fração das requisições originais — é a ferramenta padrão para isso, mas só funciona quando o orçamento é aplicado no cliente e o servidor coopera com dicas de backoff (um cabeçalho Retry-After, um payload de erro estruturado que carrega a decisão de prioridade de volta).
O quarto é aquele em que a Uber gastou mais tempo: onde o loop de controle vive. Iterações anteriores colocavam o controle em uma camada de roteamento sem estado na frente do Docstore. A camada de roteamento não conseguia ver a carga em nível de partição a tempo. Quando ela descobria que uma partição estava quente, a partição já estava saturada. A correção da Uber foi colocalizar o controle de admissão com o próprio nó de armazenamento: cada nó toma sua própria decisão com base em sua própria latência, profundidade de fila, contagem de goroutines, pressão de memória e sinais de I/O. O loop de controle unificado combinou estes como entradas plug-in sob o que eles chamam de Bring Your Own Signal. A arquitetura importa: um load shedder rodando a cinco saltos do gargalo é principalmente um dashboard.
Os números da Uber daquele redesenho fazem o caso concretamente: throughput sob sobrecarga subiu 80%, latência p99 em operações de upsert caiu 70%, contagem de goroutines caiu 93%, pico de uso de heap caiu 60%. Nenhum desses ganhos veio de adicionar capacidade. Vieram de rodar a capacidade existente corretamente sob estresse.
Como eu realmente conectaria isso
Puxando essas peças para algo que um único dono de serviço poderia entregar no próximo sprint, a ordem que eu seguiria é pequena e concreta:
- Substitua qualquer teto de RPS por serviço por um limite adaptativo de concorrência em processo. Vegas ou Gradient2 são defaults razoáveis; ambos vêm na biblioteca da Netflix e foram portados amplamente (o filtro de concorrência adaptativa do Envoy usa um algoritmo no estilo Gradient, o Resilience4j tem uma porta, serviços Go tendem a pegar uma das implementações open-source de AIMD ou Vegas).
- Marque cada requisição entrante com uma prioridade. Duas camadas são suficientes para começar: interativa vs em segundo plano. O trabalho difícil é propagar a tag pelos metadados RPC para que serviços downstream a herdem.
- Estabeleça pisos de partição. Reserve pelo menos 10% do limite para tráfego de baixa prioridade para que ele não passe fome em incidentes longos.
- Retorne rejeições estruturadas. Um 429 puro não diz nada. Inclua a prioridade em que a decisão foi feita, uma dica de
Retry-After, e metadados suficientes para os clientes recuarem inteligentemente. Sem isso, os clientes vão fingir que a rejeição não aconteceu. - Acompanhe descarte-por-prioridade como uma métrica de primeira classe. O que alertar não é "descarte ativo" — esse é o sistema funcionando — mas "descarte acima da camada N", porque esse é o ponto em que o tráfego voltado ao usuário começa a perder.
O que eu não faria na primeira passagem: construir um serviço complexo de quota por tenant, implantar isso na frente dos serviços em vez de dentro deles, ou lançar seis camadas no primeiro dia. A versão barata disso — limite adaptativo mais duas prioridades mais um piso de partição — captura a maior parte do ganho disponível. A versão cara é o que a Netflix e a Uber construíram depois de anos rodando a versão barata em produção e aprendendo onde ela cede.
Quando alcançar isso — e quando pular
Use load shedding baseado em prioridade quando o serviço tem tráfego heterogêneo onde algumas chamadas são visivelmente mais importantes que outras: leituras voltadas ao usuário vs analytics em segundo plano, escritas síncronas vs assíncronas, clientes pagantes vs camada gratuita, control plane vs data plane. Use quando retries dos clientes já fazem parte do modo de falha sob estresse. Use quando o autoscaling é lento demais para cobrir os bursts que de fato machucam, que é a maior parte do autoscaling, na maior parte do tempo.
Pule quando o tráfego é uniforme — toda requisição é o mesmo tipo de leitura voltada ao cliente, com o mesmo custo — porque não há taxonomia para traçar uma linha. Nesse caso, concorrência adaptativa sozinha, sem prioridade, dá a maior parte do benefício. Pule quando o gargalo está downstream e não há forma de empurrar prioridade através dele, porque então a prioridade está sendo decidida com base em informação ruim. E pule quando o sistema consegue absorver 30 segundos de degradação sem impacto no negócio, porque a sobrecarga de rodar e manter a taxonomia é real e nem todo serviço vale isso.
Recapitulando:
- Limites estáticos de RPS limitam a variável errada; concorrência é o que satura um serviço, e a Lei de Little torna explícita a relação entre RPS, latência e concorrência.
- Limites adaptativos de concorrência guiados por latência (Vegas, Gradient2) movem o teto sozinhos e absorvem os segundos que o autoscaling não consegue.
- Uma pequena taxonomia de prioridades que viaja com a requisição permite ao sistema descartar de baixo para cima sob sobrecarga em vez de falhar para todos uniformemente.
- Classificação incorreta, inanição de baixa prioridade, tempestades de retries e posicionamento do loop de controle são as quatro formas em que esse design sai pela culatra em produção; cada uma tem uma mitigação conhecida.
- Comece com limite adaptativo mais duas camadas mais um piso de partição. A versão cara é o que grandes frotas entregam, mas a versão barata captura a maior parte do benefício.
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
Posts Relacionados
Actor-per-Entity vs Bloqueio Otimista no Postgres: Um Comparativo em Reserva de Assentos
Executei a mesma carga de trabalho de reserva de assentos com hot key de duas formas: Postgres com coluna de versão e retries, e um único actor por assento. O design com actor não escalou melhor — ele moveu o problema difícil do controle de concorrência para a corretude de roteamento e rebalanceamento, e essa troca foi a mais fácil de raciocinar sob hot keys.
Auditando um serviço Scala contra as quatro restrições regenerativas de Chad Fowler
Levei um serviço Scala de processamento de pedidos das minhas anotações pelas quatro restrições regenerativas de Chad Fowler. Duas passaram de graça, duas forçariam um redesign de verdade. Aqui está o que aprendi sobre onde "módulo fracamente acoplado" termina e "componente regenerativo" começa, e quais partes do redesign eu de fato pagaria.
Arquitetura Baseada em Células Não É de Graça: O Que Slack, DoorDash e Roblox Realmente Pagaram Por Ela
Arquitetura baseada em células contém o raio de impacto, mas não é gratuita. Um olhar sobre o que Slack, DoorDash e Roblox realmente pagaram por células em produção — e um checklist para os padrões mais baratos de isolamento de falhas que a maioria dos times deveria considerar primeiro.