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:
Lé o número médio de operações no sistema (concorrência)λé a taxa de chegada (throughput)Wé o tempo médio que cada operação fica no sistema (latência)
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.
// 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).
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.
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.
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.
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
- 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.
- 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.
- 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
- paper A Proof for the Queuing Formula L = λW — John D. C. Little (Operations Research, 1961).
- artigo How NOT to Measure Latency — Gil Tene (Strange Loop, 2015).
- artigo The RED Method — Tom Wilkie (Grafana Labs blog, 2018).
- artigo The USE Method — Brendan Gregg (brendangregg.com, 2012).
- livro Systems Performance: Enterprise and the Cloud (2ª ed.) — Brendan Gregg (Pearson, 2020).
- livro Site Reliability Engineering — Betsy Beyer et al., Google (O'Reilly, 2016).
- livro The Site Reliability Workbook — Google (O'Reilly, 2018).
- livro Performance Modeling and Design of Computer Systems — Mor Harchol-Balter (Cambridge, 2013).
- docs HDR Histogram.
- docs Prometheus — Histograms and Summaries.
- docs OpenTelemetry — ExponentialHistogram.
- vídeo Why Averages Are Inadequate — Theo Schlossnagle (Surge, 2010).