Regras de Async & Event Loop
O Node serve I/O concorrente em um único thread por processo. Estas regras mantêm o event loop responsivo sob carga.
Receita
Cartão de referência rápida - pronto para copiar e colar.
import { readFile } from "node:fs/promises";
import pLimit from "p-limit";
const limit = pLimit(10);
export async function processBatch(ids: string[]) {
await Promise.all(ids.map((id) => limit(() => handleOne(id))));
}Quando usar isso:
- Manipuladores de requisição realizam trabalho de arquivo, rede ou CPU.
- Workers processam mensagens de fila em paralelo.
- Picos de latência aparecem sob carga concorrente.
Exemplo de Trabalho
// src/files/serve-config.ts - BOM
import { readFile } from "node:fs/promises";
let cached: string | null = null;
export async function getConfig(): Promise<string> {
if (cached) return cached;
cached = await readFile(new URL("./defaults.json", import.meta.url), "utf8");
return cached;
}// RUIM - bloqueia o event loop por requisição
import { readFileSync } from "node:fs";
export function getConfigSync() {
return readFileSync("./defaults.json", "utf8");
}// src/workers/outbound.ts
import pLimit from "p-limit";
const httpLimit = pLimit(20);
export async function notifyAll(urls: string[]) {
await Promise.all(
urls.map((url) =>
httpLimit(async () => {
const res = await fetch(url, { signal: AbortSignal.timeout(5_000) });
if (!res.ok) throw new Error(`notify failed ${res.status}`);
}),
),
);
}O que isso demonstra:
- Leitura assíncrona de arquivo com cache em nível de módulo após o primeiro carregamento.
p-limitlimita o HTTP de saída concorrente para 20.AbortSignal.timeoutprevine Promises pendentes.
Mergulho Profundo
Como Funciona
- JavaScript roda em um único thread; trabalho síncrono longo atrasa todas as requisições.
- I/O assíncrono delega para o pool de threads do libuv / kernel; callbacks retomam no loop.
Promise.allem 10.000 tarefas ainda agenda 10.000 operações - concorrência limitada.- Trabalho intensivo de CPU pertence a
worker_threadsou workers externos.
Regras em Resumo
| Regra | Justificativa |
|---|---|
| Sem fs/crypto síncrono em manipuladores | Bloqueia o loop para todos os clientes |
Sempre await em promises | Erros assíncronos não tratados e bugs de "fire-and-forget" |
| I/O de saída paralelo limitado | Protege sockets e upstreams |
| Timeouts em chamadas de rede | Previne que requisições travadas ocupem memória |
| CPU crunch fora da thread principal | JSON parse de payload de 50MB bloqueia todo mundo |
Notas de TypeScript
- Habilite
@typescript-eslint/no-floating-promisesem CI. - Type
limitwrappers retornamPromise<T>de tarefas limitadas.
Armadilhas
- Async "fire-and-forget" -
void doWork()perde erros. Correção:await,.catchcom log, ou fila de jobs. - Bcrypt síncrono na rota de autenticação - 100ms de CPU por login sob carga. Correção: hash assíncrono ou pool de workers.
Promise.allilimitado no DB - Esgota o pool de conexões. Correção: tamanho do pool + limite de concorrência alinhados.- Estrelamento de microtask - Loop infinito de
queueMicrotask. Correção: revisão de código; nunca microtasks síncronas recursivas. - Bloqueio de
JSON.parseem corpo enorme - CPU síncrona efetiva. Correção: limites de tamanho no proxy reverso e middleware de parser.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
worker_threads | Transformações CPU-bound | API CRUD simples |
| Fila externa (BullMQ) | Trabalho assíncrono pesado | Trabalho inline < 50ms |
| Módulo Cluster | Escala de CPU em host único | APIs I/O-bound (use pods horizontais) |
FAQs
fs.promises é sempre não-bloqueante?
Ele descarrega para o pool de threads; ainda tem tamanho de pool limitado. Cacheie pequenos arquivos de configuração; stream de arquivos grandes.
Qual o limite de concorrência para fetch?
Comece com 10-50 com base na tolerância do upstream; monitore 429/503 e ajuste.
Fastify lida com concorrência de forma diferente?
Mesmas regras de event loop; Fastify reduz o overhead por requisição, mas não remove o risco de código síncrono bloqueante.
Como detectar lag no event loop?
perf_hooks.monitorEventLoopDelay ou métricas de event loop APM em produção.
Interceptors NestJS são seguros para async?
Sim, se eles usarem await; evite trabalho síncrono em interceptors e guards.
setImmediate vs process.nextTick?
Prefira setImmediate para adiar trabalho; nextTick roda antes do I/O e pode famintos o loop se mal utilizado.
Streams para arquivos grandes?
Use fs.createReadStream em vez de ler o arquivo inteiro para a memória.
Promise.all vs allSettled?
all falha rapidamente no primeiro erro; allSettled para notificações em lote onde sucesso parcial é aceitável.
Como timers afetam o shutdown?
Limpe intervalos em SIGTERM; veja runbooks de graceful shutdown na seção runtime-ops.
Top-level await é aceitável?
Sim em módulos ESM para carregar configuração na inicialização; não no caminho de requisição.
Relacionado
- Checklist de Regras de Projeto Node - regras 4-5
- Regras de API - regras de timeout para manipuladores
- Regras de Logging & Observabilidade - métricas de lag do loop de log
Versões do Stack: Esta página foi escrita para Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, e NestJS 11.