MÓDULO 06 · CONCEITO 03 DE 12

Latência vs throughput

Duas métricas que parecem irmãs e respondem coisas diferentes. Little's Law (1961), distribuições assimétricas, percentis em vez de médias, e por que "latência média 50ms" é a métrica mais enganosa do dicionário operacional.

Tempo de leitura ~22 min Pré-requisito Conceito 02 (pilha de latência) Próximo Tail latency

Em entrevistas de design e em revisões de SLA, a confusão entre latência e throughput aparece em quase toda conversa não-trivial. Alguém pergunta "esse sistema aguenta 1000 RPS?" e a resposta volta como "sim, latência média de 100 ms". Outra pessoa pergunta "qual a latência desse endpoint?" e a resposta volta como "5000 requests por minuto". As duas respostas são tecnicamente possíveis, e as duas estão respondendo a perguntas diferentes das que foram feitas. Sêniores que entendem a distinção articulam claramente; sêniores que não entendem acumulam decisões de design baseadas em métricas erradas.

Latência é o tempo que uma operação individual leva. É medida por operação, e em sistemas reais é uma distribuição, não um valor único. Throughput é quantas operações por unidade de tempo o sistema consegue completar. É medida por janela temporal, agregada. As duas se relacionam por uma equação fundamental — a Lei de Little, formulada por John D. C. Little em 1961 e provada como universal —, mas a relação não é o que a intuição sugere. Maior throughput não significa menor latência; pode significar o oposto.

A confusão tem uma raiz prática: as duas métricas se confundem em sistemas pequenos. Se a sua API atende uma request por vez, "latência" e "tempo entre conclusões" coincidem. Quando o sistema escala — concorrência, paralelismo, fila — as duas se separam, e ignorar a separação leva a otimizações que melhoram uma à custa da outra sem que ninguém perceba. O exemplo clássico: batching melhora throughput drasticamente e prejudica latência percebida — o que é vitória num pipeline ETL e desastre numa API web.

Este conceito articula a distinção, formaliza Little's Law, mostra por que distribuições de latência são quase sempre assimétricas (e por que isso é o que torna "média" enganosa), e enuncia a régua mental: monitorar percentis (P50, P95, P99, P99.9), nunca apenas média ou apenas máximo. O conceito 04 detalha o que acontece na cauda da distribuição.

Definições rigorosas — e por que importam

Latência é a duração de uma operação, do início (chegada da request) até o fim (entrega completa da resposta). Em sistemas web, é típico definir como "tempo desde que o servidor recebeu o byte do request até quando enviou o último byte da response", mas a definição precisa varia: o cliente pode medir diferente (incluindo TLS handshake, DNS, etc.). Sempre vale articular onde a medição começa e termina.

Throughput é o número de operações completadas por unidade de tempo. Em web, requests por segundo (RPS) ou requests per minute (RPM); em processamento batch, registros por hora; em transação financeira, TPS (transactions per second). O detalhe importante: throughput sem qualificação de qualidade (latência aceitável, taxa de erro aceitável) não significa muito. "Aguenta 10 mil RPS" pode ser verdadeiro a custo de P99 de 30 segundos — número que ninguém quer pagar.

Por isso a forma rigorosa de reportar capacidade é sempre conjunta: "X RPS sustentável a P99 < Y ms com error rate < Z%". As três variáveis são necessárias; qualquer uma sozinha engana.

Lei de Little — a equação universal

John D. C. Little, professor do MIT, publicou em 1961 no Operations Research um teorema simples: em qualquer sistema em estado estacionário, L = λ × W, onde:

A prova de Little é geral — funciona para qualquer sistema com chegada e saída em equilíbrio, sem assumir distribuição específica. Em prática, isso significa: concorrência é throughput vezes latência. Se você quer 1000 RPS com latência média de 100 ms, precisa de capacidade para manter 100 operações simultâneas (1000 × 0.1). Se a capacidade efetiva é 50 (limitada por threads, conexões, connection pool), throughput tem teto de 500 RPS — independente de o hardware ser rápido.

A consequência prática é que latência e throughput se ligam por capacidade de concorrência. Sistema assíncrono (Node, Go, asyncio) suporta milhares de operações simultâneas com pouco overhead, então sustenta throughput alto mesmo com latência média não-trivial. Sistema síncrono com pool de threads pequeno (Java tradicional pré-Loom, .NET com bloqueio) tem L baixo, e throughput cai linearmente quando latência sobe.

A pergunta "como esse sistema reage a aumento de latência da dependência?" tem resposta direta via Little: se latência média sobe de 100 ms para 200 ms e a capacidade de concorrência é fixa, throughput cai pela metade. É por isso que dependências lentas matam throughput proporcionalmente.

Distribuições — por que média é traiçoeira

Latência em sistemas reais raramente é gaussiana (distribuição normal). É quase sempre assimétrica à direita — a maioria das operações é rápida, e uma cauda longa de operações lentas estica a média muito além do que a maioria experimenta. Em alguns sistemas, a distribuição é multimodal — dois ou três picos representando comportamentos diferentes (cache hit vs miss, query simples vs query complexa).

Considere um exemplo concreto. Sistema com 100 requests: 90 respondem em 10 ms; 10 respondem em 1000 ms (porque caíram em path lento — query sem índice, GC pause, dependência degradada). A média é (90 × 10 + 10 × 1000) / 100 = 109 ms. A mediana (P50) é 10 ms. P90 é tipicamente o último request rápido — também 10 ms. P95 pode ser 1000 ms (depende da posição dos lentos). A média sozinha esconde que 10% dos usuários experienciam 100× mais latência que os 90% típicos.

Pior: dois sistemas com a mesma média podem ter experiências de usuário radicalmente diferentes. Sistema A com latência uniforme de 109 ms é totalmente diferente de Sistema B com a distribuição acima — e os dois reportam "média 109 ms". Métrica que esconde essa diferença não é métrica útil; é métrica enganosa.

A solução é monitorar percentis. P50 (mediana) — quanto a metade típica experimenta. P95 — o que 95% dos usuários experienciam ou melhor. P99 — o que 99% experienciam ou melhor; equivalentemente, 1% dos usuários experiencia pior que esse valor. P99.9 — em sistemas com SLA agressivo ou com fan-out alto, importa porque o usuário pode tocar em centenas de operações em uma sessão, e experimentar P99.9 várias vezes.

Histogramas — a forma correta de armazenar latência

Calcular percentis exige armazenar a distribuição, não só agregados. Armazenar média e desvio-padrão é insuficiente — assume gaussiano. Armazenar todos os valores é caro em escala (1000 RPS × 24h = 86 milhões de pontos por dia). A solução prática é histogram — buckets pré-definidos, contar quantas operações caem em cada bucket.

Gil Tene (criador do Azul Zing JVM) formalizou em 2013 o HDR Histogram (High Dynamic Range), uma estrutura que armazena percentis com precisão configurável em qualquer faixa, com pegada de memória modesta (poucos KB para 7 ordens de magnitude com 3 dígitos significativos). É o padrão de métrica em sistemas que levam latência a sério: Cassandra, Kafka, Akka usam HDR Histogram internamente.

Prometheus tem Histogram (buckets configuráveis) e Summary (percentis pré-calculados client-side). OpenTelemetry tem ExponentialHistogram (buckets dinâmicos, similar a HDR). A escolha entre os tipos é técnica: Histogram é melhor para agregar entre instâncias (somar buckets); Summary é mais preciso por instância mas não agrega corretamente.

Tipos de distribuição que aparecem

Reconhecer a forma da distribuição ajuda a diagnosticar. Quatro tipos aparecem com frequência.

Log-normal. A forma mais comum em sistemas web. Pico em valores baixos, cauda longa. Distribuição típica de "operação que tem muitos caminhos rápidos e poucos caminhos lentos". P50 e média são próximos; P99 está bem afastado.

Bimodal. Dois picos. Tipicamente cache hit (muito rápido) e cache miss (muito mais lento). Diagnosticar bimodalidade é diagnosticar a origem dos dois picos — eles refletem caminhos qualitativamente diferentes na execução.

Multimodal com picos discretos. Três ou mais picos. Frequentemente sintoma de alguns caminhos: super rápido (cache), médio (banco quente), lento (banco frio ou query pesada), muito lento (timeout ou retry). Conhecer esses picos é mapear a topologia de caminhos do sistema.

Cauda anormal. P50 normal, mas P99 ou P99.9 totalmente fora de escala. Sintoma de GC pause, throttling, contention rara. Conceito 04 é dedicado a essa cauda.

Latência vs throughput em decisão de design

A pergunta "otimizo latência ou throughput?" tem resposta diferente conforme o sistema. Articular qual é a métrica relevante antes de otimizar evita esforço perdido — e às vezes evita degradar a métrica que importava.

Sistemas voltados a humanos (web, mobile, voz) priorizam latência. O usuário sente cada milissegundo de P99. Ganho de throughput a custo de latência (batching grande, queue depth alta) é tipicamente prejuízo.

Sistemas voltados a processamento batch (ETL, ML training, geração de relatório) priorizam throughput. Quanto custa por registro processado, quantos por hora. Ganho de latência a custo de throughput (processar um por vez) é prejuízo. Batching grande, paralelismo alto, queue depth profundo — tudo legítimo.

Sistemas mistos (que fazem os dois, como pipelines streaming com SLO de latência por mensagem) precisam articular SLO em cada dimensão. Kafka consumer com SLO "P99 < 1s do produce até consume completar" e "throughput sustentável de X mb/s" — as duas métricas medidas, as duas com alarme separado.

O experimento que mostra Little's Law em ação

A Lei de Little é abstrata até você ver os números acontecerem. O experimento abaixo monta um servidor mínimo com latência artificial e mede o que ocorre quando a concorrência muda.

C# — servidor com latência artificial + cliente concorrente
// servidor (ASP.NET Core minimal)
app.MapGet("/work", async (CancellationToken ct) => {
    await Task.Delay(100, ct);            // simula 100ms de trabalho
    return Results.Ok("done");
});

// cliente — varia concorrência
async Task BenchAsync(int concurrency, int totalRequests)
{
    var sw = Stopwatch.StartNew();
    var sem = new SemaphoreSlim(concurrency);
    var http = new HttpClient { BaseAddress = new Uri("http://localhost:5000") };

    var tasks = Enumerable.Range(0, totalRequests).Select(async _ => {
        await sem.WaitAsync();
        try { await http.GetAsync("/work"); }
        finally { sem.Release(); }
    });
    await Task.WhenAll(tasks);
    sw.Stop();

    var rps = totalRequests / sw.Elapsed.TotalSeconds;
    Console.WriteLine($"concorrência={concurrency,3} → {rps,7:F1} RPS, " +
                       $"total {sw.Elapsed.TotalSeconds:F2}s");
}

// rode com 1, 10, 50, 100 — observe como Little's Law prevê
foreach (var c in new[] { 1, 10, 50, 100 })
    await BenchAsync(c, 1000);

Com latência fixa de 100 ms (W=0.1), Little's Law prevê: concorrência 1 → 10 RPS; 10 → 100 RPS; 50 → 500 RPS; 100 → 1000 RPS. Observado bate com previsto até atingir teto físico do servidor (CPU, connection pool).

Python — asyncio.Semaphore para limitar concorrência
import asyncio, time
import httpx

async def fazer_request(client, sem):
    async with sem:
        await client.get("/work")

async def bench(concurrency: int, total: int):
    sem = asyncio.Semaphore(concurrency)
    async with httpx.AsyncClient(base_url="http://localhost:8000") as c:
        start = time.perf_counter()
        await asyncio.gather(*[fazer_request(c, sem) for _ in range(total)])
        elapsed = time.perf_counter() - start
    rps = total / elapsed
    print(f"concurrency={concurrency:3} → {rps:7.1f} RPS, total {elapsed:.2f}s")

async def main():
    for c in [1, 10, 50, 100]:
        await bench(c, 1000)

asyncio.run(main())

Em Python asyncio, concorrência alta é barata (event loop single-thread cooperativo). O experimento confirma a lei de Little — throughput cresce linearmente com concorrência até o servidor saturar.

Go — semaphore via buffered channel
package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)

func bench(concurrency, total int) {
    sem := make(chan struct{}, concurrency)
    var wg sync.WaitGroup
    start := time.Now()

    for i := 0; i < total; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            sem <- struct{}{}
            defer func() { <-sem }()

            resp, _ := http.Get("http://localhost:8080/work")
            io.Copy(io.Discard, resp.Body)
            resp.Body.Close()
        }()
    }
    wg.Wait()
    elapsed := time.Since(start)
    rps := float64(total) / elapsed.Seconds()
    fmt.Printf("concurrency=%3d → %7.1f RPS, total %.2fs\n",
        concurrency, rps, elapsed.Seconds())
}

func main() {
    for _, c := range []int{1, 10, 50, 100} { bench(c, 1000) }
}

Em Go, goroutine + channel buffered como semáforo é idiomático. O resultado segue Little's Law e expõe o teto do servidor onde escalonamento M:N de goroutines começa a saturar.

O que acontece quando o servidor satura

Little's Law é elegante até o servidor encontrar seu teto. A partir daí, aumentar concorrência não aumenta throughput — aumenta latência. Cada operação adicional fica em fila esperando recursos (CPU, conexão de banco, memória) que estão saturados. A relação típica:

Em baixa concorrência, latência é estável (cada operação tem recurso à vontade) e throughput cresce linearmente.

No knee point (joelho da curva), throughput começa a estabilizar e latência começa a subir. É o ponto onde algum recurso saturou. Identificar o knee é parte de capacity planning.

Em saturação, throughput fica plano (ou até cai por overhead de coordenação) e latência cresce sem teto — a fila enche, e operações esperam cada vez mais. P99 explode.

A consequência é que sistemas em produção devem operar antes do knee, com folga. Operar perto do knee é instável: pequena flutuação de tráfego empurra para saturação, e P99 desestabiliza. A regra prática de SRE do Google: utilização média de CPU em torno de 50–70% em horário de pico, deixando 30–50% de folga para picos e degradação parcial.

USE method e RED method — duas formas de estruturar métricas

Brendan Gregg propôs em 2012 o USE method: para cada recurso (CPU, memória, disco, rede), monitorar Utilization (% do tempo ocupado), Saturation (fila ou espera) e Errors (falhas). É método focado em recursos — útil para diagnóstico de gargalo de hardware/SO.

Tom Wilkie (cofundador da Grafana Labs) propôs em 2017 o RED method: para cada serviço, monitorar Rate (requests por segundo), Errors (taxa de erro), e Duration (latência). Método focado em serviços — útil para SLO de aplicação.

Os dois se complementam. RED responde "como o serviço está respondendo aos usuários?". USE responde "o que no sistema está saturando?". Sêniores que tocam ops tipicamente operam com painéis dos dois lado a lado.

armadilha em produção

Otimização que melhora throughput e prejudica P99 sem ninguém perceber. Cenário recorrente: equipe introduz batching agressivo (acumular 100 requests antes de processar) para "melhorar throughput". Throughput sobe 3x — vitória aparente. P50 sobe ligeiramente; P99 sobe drasticamente porque agora cada request tipicamente espera batch encher antes de processar. Métrica reportada para liderança é throughput; usuário sente P99. Quando a reclamação chega, ninguém conecta com a mudança feita 6 meses atrás. Defesa: SLO articulado em ambos eixos. Toda mudança que mexe em qualquer um precisa verificar o outro.

Coordinated omission — o erro de medição que esconde o pior

Gil Tene cunhou em 2013 o termo coordinated omission para um erro sistêmico de medição que ocorre em quase todas as ferramentas de load test tradicionais. O cenário: a ferramenta envia uma request, espera resposta, registra latência, envia próxima. Quando o sistema fica lento, a ferramenta também espera mais — e portanto envia menos requests durante o período lento. As requests "perdidas" não entram no histograma.

O problema: se o sistema teve um GC pause de 2 segundos, a ferramenta registrou um único request de 2 segundos — mas em produção, durante esses 2 segundos, deveriam ter chegado dezenas de requests, e todas experienciariam latência alta (algumas próximas de 2 segundos, esperando a fila esvaziar). A ferramenta ingênua reporta "tudo bem com P95 de 50ms"; a realidade é P95 de 1.5 segundos.

Ferramentas modernas (k6, wrk2, hdrhistogram-based) compensam coordinated omission registrando latência desde o tempo esperado de início (não desde o tempo real de envio). É detalhe técnico, mas a diferença em P99 reportado pode ser de 10× ou mais. Sêniores que avaliam SLO precisam saber se a ferramenta usada compensa.

heurística do sênior

Toda métrica de latência deve aparecer como distribuição: P50, P95, P99, P99.9 mínimo. Métrica agregada como "média" só vale como sanity-check secundário. Em SLO, não defina "latência média < 100ms"; defina "P99 < 200ms a X RPS". Em capacity planning, não chute pelo throughput; calcule via Little's Law (throughput = concorrência / latência) e deixe folga para o knee. Em discussão de melhoria, antes de aceitar "ficou mais rápido", peça os dois números — média esconde, percentis revelam.

Por que importa para a sua carreira

A distinção latência/throughput separa quem opera sistemas de quem só os escreve. Em entrevistas, "como você definiria SLO para esse serviço?" é pergunta direta — a resposta forte cita percentis, articula throughput como "X RPS sustentável a P99 < Y", e menciona Little's Law como base de capacity planning. Em revisões de PR que reportam "ficou mais rápido", pedir os dois números (latência distribuída, throughput qualificado) é serviço ao time. Em pos-mortem, articular "throughput estava ok, mas P99 estourou — saturamos antes do esperado pelo Little's Law" guia a investigação melhor que "estava lento". Em conversa com produto, traduzir "experiência ruim" em "P99 acima de 1s para 5% dos usuários" cria base comum mensurável.

Como praticar

  1. Reproduzir Little's Law experimentalmente. Implemente o experimento do lang-compare na sua linguagem principal. Plote concorrência × throughput. Encontre o knee da curva (onde throughput estabiliza). Aumente concorrência além do knee e veja P99 explodir. Anote: qual era o recurso saturando — CPU, banco, connection pool? Esse exercício torna a lei de Little viva, não decorada.
  2. Auditoria de métricas em projeto seu. Identifique quais métricas de performance o time olha. Para cada uma, classifique: é média, percentil, ou agregado? Para as que são média, proponha trocar por histograma + percentis. Para as que são throughput sem qualificação, proponha qualificar (com SLO de latência associado). Esse é o tipo de proposta que vira cultura de equipe — e evita decisões que otimizam o número errado.
  3. Calcular capacity via Little's Law. Em algum sistema seu, escolha um endpoint. Meça latência típica (P50). Meça concorrência média (quantas requests in-flight em horário de pico — via ferramenta APM ou contadores). Calcule throughput previsto pela lei (λ = L/W). Compare com throughput medido. Se diverge, descubra por quê — geralmente é que P50 não é representativo, ou L é maior do que você imaginou.

Referências para aprofundar

  1. paper A Proof for the Queuing Formula L = λW — John D. C. Little (Operations Research, 1961). O paper original. Curtinho, prova geral, sem hipóteses sobre distribuição. A elegância matemática justifica a fama.
  2. artigo How NOT to Measure Latency — Gil Tene (Strange Loop, 2015). YouTube. Tene articula coordinated omission, importância de percentis altos, e fundamentos do HDR Histogram. Indispensável para sêniores que tocam SLO.
  3. artigo The RED Method — Tom Wilkie (Grafana Labs blog, 2018). grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services — Wilkie introduz Rate, Errors, Duration como tríade canônica para serviços.
  4. artigo The USE Method — Brendan Gregg (brendangregg.com, 2012). Utilization, Saturation, Errors. Método para diagnóstico em camada física. Complementa RED.
  5. livro Systems Performance: Enterprise and the Cloud (2ª ed.) — Brendan Gregg (Pearson, 2020). Cap. 2 cobre métricas, percentis, histogramas com profundidade. Cap. 6 e 7 aplicam em sistemas reais.
  6. livro Site Reliability Engineering — Betsy Beyer et al., Google (O'Reilly, 2016). Cap. 4 (Service Level Objectives) é o tratamento canônico de como Google define SLOs em latência distribuída. Gratuito em sre.google/books.
  7. livro The Site Reliability Workbook — Google (O'Reilly, 2018). Continuação prática do SRE Book. Cap. 2 e 3 contêm exemplos concretos de SLOs definidos em percentis.
  8. livro Performance Modeling and Design of Computer Systems — Mor Harchol-Balter (Cambridge, 2013). O livro mais profundo sobre teoria de filas em sistemas computacionais. Cobre Little's Law e generalizações. Para sêniores que querem fundamento matemático.
  9. docs HDR Histogram. hdrhistogram.org — Documentação canônica do HDR Histogram com implementações em Java, C, Python, Go, .NET. Ler o README é meia tarde bem investida.
  10. docs Prometheus — Histograms and Summaries. prometheus.io/docs/practices/histograms — Página canônica que articula a diferença entre os dois tipos. Sêniores que usam Prometheus precisam ler.
  11. docs OpenTelemetry — ExponentialHistogram. opentelemetry.io/docs/specs/otel/metrics/data-model — Spec que define o histograma exponencial moderno; substitui buckets fixos com precisão configurável.
  12. vídeo Why Averages Are Inadequate — Theo Schlossnagle (Surge, 2010). YouTube. Schlossnagle (Circonus) é uma das vozes mais articuladas sobre métricas distribuídas. A palestra é antiga mas o argumento é canônico.