System Design/03 - Qualidade/Performance Antipatterns2 min
Chatty IO
- Que problema isso resolve?
- Quando usar / quando NÃO usar?
- Qual o principal trade-off?
- Como isso falha em produção?
Conceito
Muitas requisições pequenas onde poucas maiores resolveriam. O custo não está no volume de dados — está no custo fixo por operação, pago repetidamente.
Cada ida e volta carrega latência de rede, handshake, serialização, autenticação e enfileiramento. Com 1 ms de latência e 1.000 chamadas, são 1.000 ms gastos em espera pura.
Onde aparece
| Forma | Exemplo |
|---|---|
| N+1 em ORM | Carregar 100 pedidos, depois o cliente de cada um |
| Laço com chamada remota | for id in ids: api.buscar(id) |
| Leitura/escrita registro a registro | INSERT em laço em vez de lote |
| Muitas mensagens minúsculas | Overhead do protocolo domina |
| API granular demais | Uma chamada por campo |
A correção
| Técnica | Como |
|---|---|
| Lote | WHERE id IN (...), insertMany, mget |
| Junção / eager loading | Trazer o relacionado na mesma consulta |
| Endpoint composto | Uma chamada devolve o agregado (Gateway Aggregation) |
| Cache | Elimina a repetição (Caching) |
| Streaming | Um canal contínuo em vez de N requisições |
Trade-offs
A correção tem limite: agrupar demais leva ao antipadrão oposto, Extraneous Fetching — uma consulta gigante que traz muito mais do que se usa, consome memória e trava por mais tempo.
Lotes muito grandes também aumentam latência do primeiro resultado, ocupam mais memória e, em caso de falha, perdem o lote inteiro. O tamanho de lote é um parâmetro a medir, não a adivinhar — tipicamente entre 100 e 1.000 itens.
Há ainda um caso em que chatty é aceitável: quando as chamadas podem ser paralelizadas e a latência total passa a ser a da mais lenta, não a soma. Isso reduz o sintoma sem eliminar o custo — cada chamada ainda consome conexão e CPU do servidor.
Exemplo prático
O N+1 clássico, e por que ele é invisível em desenvolvimento:
pedidos = db.query("SELECT * FROM pedidos LIMIT 100")
for p in pedidos:
p.cliente = db.query("SELECT * FROM clientes WHERE id = ?", p.cliente_id)
# 101 consultas
Com o banco local (0,1 ms), isso custa 10 ms — imperceptível. Em produção, com 2 ms de latência de rede, custa 200 ms. Com 1.000 pedidos, 2 segundos.
pedidos = db.query("SELECT * FROM pedidos LIMIT 100")
ids = {p.cliente_id for p in pedidos}
clientes = db.query("SELECT * FROM clientes WHERE id IN (...)", ids)
# 2 consultas
De 101 para 2. A mudança é pequena e o efeito é de ordem de grandeza — e é por isso que N+1 é o primeiro item a procurar em qualquer trace lento.
Relacionado
Parte de Performance Antipatterns · roadmap.sh/system-design