System Design/03 - Qualidade/Performance Antipatterns2 min
Synchronous IO
- Que problema isso resolve?
- Quando usar / quando NÃO usar?
- Qual o principal trade-off?
- Como isso falha em produção?
Conceito
Bloquear uma thread esperando I/O — disco, rede, banco — quando ela poderia estar atendendo outra requisição.
Durante a espera, a thread não usa CPU: ela apenas ocupa memória (pilha de ~1 MB) e uma vaga no pool. Com I/O de 100 ms e um pool de 200 threads, o teto é 2.000 requisições por segundo, independentemente de quanta CPU exista sobrando.
O sintoma característico
CPU: 15%
Memória: 40%
Latência: 4 segundos
Baixa utilização de recurso com alta latência é a assinatura do problema: o sistema está esperando, não trabalhando.
As saídas
| Modelo | Como funciona |
|---|---|
| Async / await | A thread é liberada durante a espera |
| Event loop | Uma thread, milhares de conexões (Node, nginx) |
| Corrotinas / fibers | Threads leves (Go, Kotlin, Java 21 virtual threads) |
| Reactive streams | Fluxo não-bloqueante com back pressure |
Trade-offs
Código assíncrono é mais difícil de escrever, ler e depurar. Stack traces ficam fragmentados, o fluxo deixa de ser linear, e erros de concorrência são mais sutis. O "color problem" (funções assíncronas contaminam quem as chama) força mudanças em cascata.
Há também um limite importante: assincronia não ajuda em trabalho de CPU. Se a thread está calculando, liberá-la não é possível — o gargalo é CPU, e a resposta é paralelismo ou mais máquinas.
E um erro comum é a assincronia parcial: um único ponto bloqueante dentro de um event loop trava tudo. Uma chamada síncrona ao disco num servidor Node paralisa todas as conexões que aquele loop atende.
Virtual threads (Java 21) e goroutines mudam o cálculo: entregam o ganho de escalabilidade mantendo o código com aparência síncrona — o melhor dos dois mundos, quando disponíveis na plataforma.
Exemplo prático
# bloqueante: a thread fica parada 300 ms
def handler():
a = requests.get(url_a) # 100 ms parado
b = requests.get(url_b) # 100 ms parado
c = db.query(sql) # 100 ms parado
return combinar(a, b, c)
# assíncrono e paralelo: 100 ms, thread livre durante a espera
async def handler():
a, b, c = await asyncio.gather(
fetch(url_a), fetch(url_b), query(sql))
return combinar(a, b, c)
Duas melhorias distintas acontecem aqui. A paralelização reduz a latência de 300 ms para 100 ms — ganho visível ao usuário. A não-blocagem libera a thread durante a espera — ganho invisível ao usuário e decisivo para o throughput: a mesma máquina passa a atender ordens de grandeza mais requisições simultâneas.
Relacionado
Parte de Performance Antipatterns · roadmap.sh/system-design