MÓDULO 07 · CONCEITO 09 DE 12

Backpressure & elastic systems

Reactive Manifesto, quando absorver vs rejeitar carga, queue depth como sinal, graceful degradation. Conexão com módulo 04 conceito 12 (backpressure local) — aqui elevado para sistema distribuído inteiro.

Tempo de leitura ~22 min Pré-requisito Módulo 04 conceito 12 (backpressure) Próximo Multi-tenancy

Em julho de 2014, um grupo de engenheiros liderado por Jonas Bonér (Lightbend, criador do Akka) publicou o Reactive Manifesto v2 em reactivemanifesto.org. Quatro princípios orientadores para sistemas modernos: Responsive, Resilient, Elastic, Message Driven. Entre os detalhes do manifesto, um conceito apareceu como pedra angular: backpressure. A tese: sistemas distribuídos sob carga precisam de mecanismo para sinalizar ao produtor que o consumidor está saturado — sem esse sinal, sistema colapsa.

O módulo 04 conceito 12 cobriu backpressure no nível local — entre threads de uma aplicação, entre stages de um pipeline interno. Esse conceito eleva para sistema distribuído inteiro. Quando worker consome de fila Kafka, e o consumer não consegue acompanhar, o que acontece? Quando aplicação recebe carga acima da capacidade, e a única opção é "absorver tudo", como ela protege os recursos downstream? Quando sistema satura, ele quebra ou degrada graciosamente?

A resposta certa quase sempre é "rejeitar carga excedente em vez de absorver". O instinto contrário — "tentar fazer tudo, esperar que dê certo" — é o que produz cascading failures (módulo 08 e SRE livro do Google). Sistemas elásticos se definem pela capacidade de recusar graciosamente quando saturados, e voltar ao normal quando carga reduz. Esse é o princípio central deste conceito.

Articularemos: o problema do "absorver tudo", os mecanismos de rejeição (rate limiting, queue bounded, circuit breaker à montante), graceful degradation, load shedding adaptativo, e back-pressure em pipelines streaming. O escopo é pragmático — quais técnicas aplicar em quais camadas do sistema.

O problema — "absorver tudo" é antipadrão

Imagine um sistema que recebe 1000 RPS de carga normal e 5000 RPS de pico. Capacidade sustentada: 2000 RPS. Quando pico chega:

Cenário "absorver tudo": aceita todas as 5000 requests. Elas entram em fila interna (queue, thread pool, connection pool). Backlog cresce. Latência sobe — primeiro 100 ms, depois 500 ms, depois 5 s. Algumas viram timeout no cliente; cliente reenvia (retry); carga sobe mais. Sistema fica preso processando carga antiga enquanto nova chega. Eventualmente, esgota memória, banco satura, ou serviço crash. Recovery exige restart manual. Tempo de degradação: 30 minutos a horas.

Cenário "rejeitar excedente": sistema reconhece capacidade. Ao primeiro sinal de saturação (CPU alto, queue cheia, latência subindo), começa a rejeitar requests novas com erro explícito (HTTP 429 Too Many Requests, ou 503 Service Unavailable). Cliente recebe erro, retenta com backoff (módulo 05 conceito 9), ou degrada UX. Sistema mantém capacidade sustentada, latência permanece baixa para requests aceitas. Quando pico passa, sistema volta ao normal sem intervenção. Tempo de degradação: minutos.

A diferença é qualitativa. "Absorver tudo" é cortês inicialmente — toda request recebe tentativa — e catastrófico no fim. "Rejeitar excedente" é grosseiro pontualmente — algumas requests recebem erro — e robusto no agregado. Sistemas maduros escolhem o segundo, e essa escolha define maturidade arquitetural.

Os mecanismos de proteção

Há quatro mecanismos canônicos para implementar "rejeitar excedente". Cada um opera em camada diferente.

Rate limiting na borda

Limita quantas requests por unidade de tempo cada cliente (ou globalmente) pode fazer. Se exceder, retorna 429 imediatamente. Implementado em load balancer (NGINX limit_req, Cloudflare Rate Limiting), ou na aplicação (Polly RateLimiter, FastAPI middleware, custom).

Algoritmos comuns: token bucket (cada cliente recebe N tokens por janela; cada request consome 1; quando esgota, rejeita até reabastecimento), leaky bucket (taxa fixa de saída; excesso é descartado), sliding window (variação que considera distribuição temporal).

Rate limit por cliente protege de cliente abusivo; rate limit global protege o sistema. Combinar os dois é padrão.

Queue bounded com rejeição

Em pipelines com fila, configure capacidade máxima da fila. Quando cheia, comportamentos: rejeitar (retornar erro), bloquear produtor (espera até ter espaço), descartar mais antiga (drop oldest), ou descartar nova (drop newest). Cada estratégia tem semântica própria.

Para APIs, "bloquear produtor" raramente é apropriado — produtor é cliente HTTP, e bloquear vira espera longa. "Rejeitar nova" é tipicamente a opção (retorna 503 imediatamente). Para pipelines internos (worker → worker), bloqueio pode ser ok (backpressure clássico).

Kafka tem max.in.flight.requests.per.connection; RabbitMQ tem prefetch limit; SQS tem max in-flight messages. Configurar adequadamente força backpressure natural.

Circuit breaker contra dependências saturadas

Já visto no módulo 05 conceito 9. Quando dependência downstream (banco, API externa) satura, breaker abre — recusa chamadas até dependência recuperar. Sem breaker, aplicação queima recursos esperando dependência morta, e propaga lentidão.

Em escala distribuída, breaker é particularmente importante. Sem ele, lentidão de uma dependência vira congelamento da aplicação inteira; com ele, a aplicação degrada graciosamente (retorna erro para requests que precisam da dependência, continua servindo as outras).

Load shedding adaptativo

A versão sofisticada. Em vez de threshold fixo ("se CPU > 80%, rejeita"), sistema mede sua capacidade em tempo real e ajusta o limite. Algoritmos como TCP Vegas (Brakmo, 1994 — origem do conceito em rede), Adaptive Concurrency Limit (Netflix, 2017), e PID controllers se aplicam.

Netflix Concurrency Limits (open source) e Envoy adaptive concurrency são exemplos práticos. Sistema observa latência e ajusta concorrência permitida. Quando latência sobe, reduz concorrência (rejeita mais); quando cai, aumenta. Auto-ajuste sem configuração manual de threshold.

Para sistemas críticos com tráfego variável, load shedding adaptativo é melhor que fixed threshold — ajusta a condições, não exige tunning frequente. Custos: complexidade, e debug mais difícil em anomalias.

Graceful degradation — UX em modo degradado

Rejeitar request é uma forma de degradação graciosa. Há outras, mais sutis.

Servir cached/stale data. Quando banco satura, servir dado de cache mesmo se ligeiramente obsoleto. Conceito 09 do módulo 06 (HTTP cache com stale-if-error) cobriu. Aplicação retorna 200 com dado antigo em vez de 503.

Desabilitar features secundárias. E-commerce sob pico: desabilita "produtos relacionados", mantém "carrinho" e "checkout". Recomendações são bonitas; checkout é essencial. Feature flags habilitam essa flexibilidade.

Reduzir qualidade. Vídeo streaming reduz bitrate sob carga. API retorna menos campos em response (só essenciais). Pagamento pula etapa de fraud check secundário e marca para revisão manual.

Diferir trabalho. Operação síncrona vira assíncrona. Cliente recebe "ok, vamos processar" em vez de erro. Trabalho real acontece em background quando capacidade volta.

Graceful degradation é decisão de produto + engenharia. Times maduros articulam quais features são "core" (sempre disponíveis) e quais são "optional" (sacrificáveis sob pressão), e implementam mecanismo de desabilitação rápida.

Cascade failures — o pior caso

Sem backpressure adequado, sistemas distribuídos podem entrar em cascade failure: uma falha (ou degradação) em uma parte do sistema propaga e amplifica em outras, até colapso geral. O SRE livro do Google dedica capítulo inteiro ao tema (cap. 22).

Padrões comuns:

Retry storm. Já visto (módulo 05 conceito 9). Dependência fica lenta, clientes retentam. Sem rate limit, retries amplificam carga até dependência morrer.

Connection pool exhaustion. Banco lento; cada conexão fica ocupada esperando. Pool esgota; novas requests timeout. Aplicação desce junto com banco.

Memory exhaustion via queue grow. Fila interna sem limite; sob carga, cresce até saturar memória. OOM killer mata processo. Recovery cold; carga recomeça; loop.

Thread pool exhaustion. Operação sincronizada que não retorna; threads acumulam até pool esgotar. Novas requests sem thread; sistema trava.

Auto-scaling cascading. Já visto no conceito 08. Auto-scaling escala devido a lentidão; novas instâncias sobrecarregam banco; banco fica mais lento; auto-scaling escala mais.

Defesas estruturais:

Sistema com todas essas defesas falha graciosamente — degrada qualidade ou rejeita excesso, mas não colapsa. Sistema sem qualquer delas pode entrar em cascade em incidente menor.

Backpressure em pipelines streaming

Em pipelines de processamento contínuo (streaming), backpressure é primitiva. Quando consumer fica lento, produtor precisa pausar naturalmente.

Frameworks modernos resolvem nativamente:

Reactive Streams (spec, 2014): protocolo padronizado entre produtor e consumer onde consumer "pulls" — produtor só envia o que consumer pediu. Implementações: RxJava, Project Reactor (Java), Akka Streams, .NET Reactive Extensions.

Apache Flink: pipelines com backpressure automática. Quando operator downstream fica lento, upstream desacelera.

Kafka consumer groups: consumer controla pace via poll(). Sem novo poll, broker não envia mais. Lag cresce no broker — visibilidade do problema.

Go channels bounded: módulo 04 cobriu. Producer bloqueia em send quando channel cheio.

Em pipelines que não têm backpressure nativa, implementar manualmente exige cuidado — limite explícito de pending work, sinal explícito quando saturado, comportamento definido em saturação.

Implementação em três stacks

Backpressure entre serviços tem padrões idiomáticos.

C# — RateLimiter + bounded channels
// .NET 7+ tem System.Threading.RateLimiting builtin
using System.Threading.RateLimiting;

// rate limit por API key
var rateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions {
    TokenLimit = 100,                   // burst
    TokensPerPeriod = 10,               // sustentado
    ReplenishmentPeriod = TimeSpan.FromSeconds(1),
    QueueLimit = 0,                     // não enfileira; rejeita imediatamente
});

// no middleware
app.Use(async (ctx, next) => {
    using var lease = await rateLimiter.AcquireAsync(1);
    if (!lease.IsAcquired)
    {
        ctx.Response.StatusCode = 429;
        ctx.Response.Headers.RetryAfter = "5";
        return;
    }
    await next();
});

// bounded channel para pipeline interno
var channel = Channel.CreateBounded<Pedido>(new BoundedChannelOptions(1000) {
    FullMode = BoundedChannelFullMode.DropWrite,    // rejeita ao chegar cheia
    // ou FullMode.Wait — bloqueia produtor (clássico backpressure)
});

// produtor
public async Task<bool> EnqueueAsync(Pedido p)
{
    return channel.Writer.TryWrite(p);  // false se cheia
}

// consumer
await foreach (var p in channel.Reader.ReadAllAsync())
    await Process(p);

.NET 7+ tem RateLimiter na biblioteca padrão. Channel.CreateBounded com FullMode escolhe a estratégia. Para API HTTP, DropWrite + 503 é típico; para pipelines internos, Wait (clássico backpressure).

Python — slowapi (Flask/FastAPI rate limit) + asyncio.Semaphore
from slowapi import Limiter
from slowapi.util import get_remote_address
from fastapi import FastAPI, HTTPException

# rate limiting por IP
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter

@app.get("/api/produtos/{id}")
@limiter.limit("100/minute")        # 100 requests por minuto por IP
async def obter_produto(id: str, request: Request):
    return await repo.obter(id)

# bounded semaphore para limitar concorrência interna
import asyncio
semaphore = asyncio.Semaphore(50)   # max 50 ops simultâneas em alguma seção

async def operacao_pesada(payload):
    if semaphore.locked():
        raise HTTPException(503, "Service overloaded")
    async with semaphore:
        return await processo_pesado(payload)

# bounded queue com rejeição
queue = asyncio.Queue(maxsize=1000)

async def producer(item):
    try:
        queue.put_nowait(item)      # rejeita imediatamente se cheia
    except asyncio.QueueFull:
        # escolha: descarta, retorna erro, etc.
        raise HTTPException(503, "Queue full")

slowapi é Limiter padrão para FastAPI/Flask. asyncio tem Semaphore e Queue bounded builtin. Para sistemas em produção, considerar rate limit centralizado em Redis (slowapi com backend Redis).

Go — golang.org/x/time/rate + bounded channels
package main

import (
    "golang.org/x/time/rate"
    "net/http"
)

// token bucket rate limiter
limiter := rate.NewLimiter(rate.Every(time.Second), 100)  // 100 RPS sustentado, 100 burst

func RateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            w.Header().Set("Retry-After", "5")
            http.Error(w, "rate limited", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// bounded channel + select com timeout para rejeição
type WorkPool struct {
    queue chan Job
}

func (p *WorkPool) Submit(j Job) error {
    select {
    case p.queue <- j:
        return nil
    case <-time.After(50 * time.Millisecond):
        return ErrQueueFull             // rejeita após pequeno wait
    }
}

// alternativa não-blocking — rejeita imediatamente
func (p *WorkPool) SubmitNonBlocking(j Job) error {
    select {
    case p.queue <- j:
        return nil
    default:
        return ErrQueueFull
    }
}

// circuit breaker já visto no módulo 05 conceito 9
// envoltório que combina rate limit + breaker + bounded queue
// gera "back-pressure trio" defensivo

Go tem golang.org/x/time/rate como biblioteca canônica de rate limit. Combinado com bounded channels (módulo 04), implementa backpressure completa. Pattern moderno: rate limit na borda + breaker em deps + bounded internal queue.

Anti-padrões frequentes

Sem rate limit em borda. API pública sem rate limit é convite para abuse e DoS. Mesmo cliente legítimo com bug pode chegar a taxa que mata sistema. Defesa: rate limit por cliente desde dia 1, mesmo se generoso.

Fila sem capacidade limite. queue := make(chan Job) sem buffer ou asyncio.Queue() sem maxsize. Cresce sem limite. OOM eventual. Defesa: sempre articular capacidade máxima.

Retry sem coordenação. Cliente retenta automaticamente; quando dependência fica lenta, retries amplificam carga. Defesa: retry com jitter e limit (módulo 05); breaker para cortar quando não vale tentar.

"Rejeição silenciosa". Sistema rejeita request mas log apenas em debug; nenhuma métrica. Time não sabe que está rejeitando. Defesa: métrica explícita de rejeições; alerta quando taxa cresce.

Configuração simétrica. Mesmo threshold para tudo. Em prática, recursos diferentes saturam em momentos diferentes; rate limit centralizado pode ser ineficaz. Defesa: rate limits por endpoint, ou sistemas de back-pressure adaptativos.

Usar 200 OK para indicar "aceito mas pode falhar". Sistema assíncrono retorna 200 OK quando coloca em fila; processamento real falha em silêncio. Cliente acha que tudo deu certo. Defesa: status apropriado (202 Accepted), e mecanismo de checagem do resultado (callback, polling, webhook).

armadilha em produção

Sistema sem backpressure entra em colapso por carga apenas modestamente acima do esperado. Cenário típico: sistema testado para 1000 RPS sustentado; em pico real chega a 1500 RPS por 10 minutos; sem mecanismo de rejeição, latência sobe; cliente retenta; carga efetiva chega a 3000 RPS; sistema congela; recovery exige restart. O incidente que deveria ser "ligeira degradação por 10 min" vira "outage de 1 hora". Defesa: rate limit + bounded queue + breaker + load shedding adaptativo garantem que sobrecarga modesta vira degradação controlada, não colapso.

heurística do sênior

Em revisão arquitetural, pergunte para cada borda de carga (HTTP, fila, cliente HTTP, conexão de banco): "o que acontece se chegar 10× mais carga que esperado?". Se a resposta é "absorvemos e ficamos lentos", está faltando backpressure. Se é "rejeitamos com erro graciosamente, log/métrica registrada, cliente sabe", o sistema está protegido. Esse exercício, aplicado sistematicamente em todo PR de novo serviço, captura a maioria dos cascades antes de produção.

Por que importa para a sua carreira

Backpressure separa sistemas que sobrevivem a pico de sistemas que colapsam. Em entrevistas de design para sistemas com pico previsto (e-commerce, mídia, fintech), "como você projeta para o pico sem capacity para o pico?" é convite direto. A resposta forte cita rate limiting, bounded queues, circuit breaker, graceful degradation, load shedding. Em revisão de proposta, identificar ausência de backpressure é serviço crítico ao time. Em pos-mortem de cascade failure, diagnosticar "falta de mecanismo de rejeição em camada X" é trabalho de senior. Em discussão de capacity planning, articular que capacity não precisa cobrir pico se há backpressure adequada muda a economia do sistema.

Como praticar

  1. Implementar trio defensivo. Em sistema seu, adicione: rate limit na borda (NGINX, Cloudflare, ou middleware da app), bounded queue interna, circuit breaker em dependência principal. Teste com k6 enviando 2-5× a capacidade. Verifique que sistema rejeita graciosamente (429/503), não colapsa. Esse exercício, raramente feito, é diferencial.
  2. Cascade simulation. Em ambiente staging, simule cascade: introduza latência em dependência (banco), mantenha carga alta. Sem breaker, observe latência subir, threads bloquear, eventualmente sistema travar. Adicione breaker; observe que degrada graciosamente. Esse experimento torna concreto o que defesas previnem.
  3. Articulação de degradation graceful. Pegue um produto seu e liste features. Para cada, classifique: core (sempre disponível) ou optional (sacrificável). Articule mecanismo (feature flag, ou circuit breaker específico) para desabilitar optional sob carga. Esse documento vira referência de operação para incidentes.

Referências para aprofundar

  1. artigo The Reactive Manifesto — Jonas Bonér et al. (2014). reactivemanifesto.org — Os princípios canônicos de sistemas reativos: responsive, resilient, elastic, message-driven. Curtinho, fundador.
  2. artigo Reactive Streams Specification (2014). reactive-streams.org — Spec do protocolo back-pressure entre componentes. Implementado em Reactor, RxJava, Akka Streams.
  3. livro Site Reliability Engineering — Beyer et al., Google (O'Reilly, 2016). Cap. 22 (Cascading Failures) é o tratamento canônico em livro técnico. Como Google evita; gratuito em sre.google/books.
  4. livro Release It! (2ª ed.) — Michael Nygard (Pragmatic Bookshelf, 2018). Já citado em módulo 05. Cap. sobre stability patterns cobre backpressure, bulkhead, circuit breaker em escala distribuída.
  5. livro Reactive Design Patterns — Roland Kuhn (Manning, 2017). O arquiteto principal do Akka cobre patterns reativos com profundidade. Cap. sobre back-pressure articula em formalismo.
  6. paper TCP Vegas: New Techniques for Congestion Detection and Avoidance — Brakmo & Peterson (SIGCOMM, 1994). A origem do conceito de congestion control adaptativo em rede. Conexão direta com adaptive concurrency.
  7. artigo Performance Under Load — Tyler Treat (bravenewgeek.com, 2017). Tratamento prático de back-pressure e load shedding em sistemas distribuídos. Inclui simulações.
  8. artigo Adaptive Concurrency Limits at Netflix — Netflix Tech Blog (2017+). netflixtechblog.com — Como Netflix implementa load shedding adaptativo. Gerou library open source concurrency-limits.
  9. artigo Using Load Shedding to Avoid Overload — AWS Builders' Library. aws.amazon.com/builders-library — Articula load shedding como prática AWS. Cobertura prática.
  10. docs System.Threading.RateLimiting (.NET). learn.microsoft.com/en-us/dotnet/api/system.threading.ratelimiting — Documentação oficial. Vários algoritmos (TokenBucket, FixedWindow, SlidingWindow, Concurrency).
  11. docs NGINX limit_req. nginx.org/en/docs/http/ngx_http_limit_req_module.html — Rate limiting na camada de proxy. Útil em todas as stacks.
  12. docs Envoy Rate Limiting. envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rate_limit_filter — Rate limit centralizado para microsserviços via service mesh.