Reglas de asincronía y bucle de eventos
Node.js maneja E/S concurrente en un solo hilo por proceso. Estas reglas mantienen el bucle de eventos receptivo bajo carga.
Receta
Tarjeta de receta de referencia rápida: lista para copiar y pegar.
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))));
}Cuándo usarlo:
- Los manejadores de solicitudes realizan trabajo de archivo, red o CPU.
- Los workers procesan mensajes de cola en paralelo.
- Aparecen picos de latencia bajo carga concurrente.
Ejemplo práctico
// src/files/serve-config.ts - BUENO
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;
}// MALO - bloquea el bucle de eventos por solicitud
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}`);
}),
),
);
}Lo que esto demuestra:
- Lectura asíncrona de archivos con caché a nivel de módulo después de la primera carga.
p-limitlimita la concurrencia de HTTP saliente a 20.AbortSignal.timeoutevita promesas colgadas.
Análisis profundo
Cómo funciona
- JavaScript se ejecuta en un solo hilo; el trabajo síncrono prolongado retrasa todas las solicitudes.
- La E/S asíncrona se delega al pool de hilos/kernel de libuv; las devoluciones de llamada se reanudan en el bucle.
Promise.allen 10,000 tareas aún programa 10,000 operaciones: concurrencia limitada.- El trabajo intensivo en CPU pertenece a
worker_threadso workers externos.
Reglas de un vistazo
| Regla | Razón |
|---|---|
| No usar fs/criptografía síncrona en manejadores | Bloquea el bucle para todos los clientes |
Siempre await promesas | Errores asíncronos no manejados y errores de "disparar y olvidar" |
| Limitar la E/S saliente paralela | Protege los sockets y los sistemas ascendentes |
| Tiempos de espera en llamadas de red | Evita que las solicitudes atascadas ocupen memoria |
| Procesamiento intensivo de CPU fuera del hilo principal | El análisis de JSON de una carga útil de 50 MB bloquea a todos |
Notas de TypeScript
- Habilitar
@typescript-eslint/no-floating-promisesen CI. - Los wrappers de
limitdeben tipar el retorno dePromise<T>de las tareas limitadas.
Errores comunes
- Async "disparar y olvidar" -
void doWork()pierde errores. Solución:await,.catchcon registro, o encolar el trabajo. - Bcrypt síncrono en ruta de autenticación - 100ms de CPU por inicio de sesión bajo carga. Solución: hash asíncrono o pool de workers.
Promise.allilimitado en DB - Agota el pool de conexiones. Solución: tamaño del pool + límite de concurrencia alineados.- Inanición de microtareas - Bucle infinito de
queueMicrotask. Solución: revisión de código; nunca microtareas síncronas recursivas. JSON.parsebloqueante en cuerpo enorme - CPU efectivamente síncrona. Solución: límites de tamaño en el proxy inverso y middleware de análisis.
Alternativas
| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
worker_threads | Transformaciones ligadas a la CPU | API CRUD simple |
| Cola externa (BullMQ) | Trabajo asíncrono pesado | Trabajo en línea < 50ms |
| Módulo Cluster | Escalado de CPU en un solo host | APIs ligadas a E/S (usar pods horizontales) |
Preguntas frecuentes
¿Es fs.promises siempre no bloqueante?
Se descarga al pool de hilos; el tamaño del pool sigue siendo limitado. Almacena en caché archivos de configuración pequeños; transmite archivos grandes.
¿Qué límite de concurrencia para fetch?
Comienza con 10-50 según la tolerancia del sistema ascendente; monitorea 429/503 y ajusta.
¿Fastify maneja la concurrencia de manera diferente?
Las mismas reglas del bucle de eventos; Fastify reduce la sobrecarga por solicitud, pero no elimina el riesgo de código síncrono bloqueante.
¿Cómo detecto el retraso del bucle de eventos?
perf_hooks.monitorEventLoopDelay o métricas de bucle de eventos de APM en producción.
¿Los interceptores de NestJS son seguros para async?
Sí, si usan await; evita el trabajo síncrono en interceptores y guards.
¿setImmediate vs process.nextTick?
Prefiere setImmediate para diferir el trabajo; nextTick se ejecuta antes de la E/S y puede agotar el bucle si se abusa.
¿Streams para archivos grandes?
Usa fs.createReadStream en lugar de leer el archivo completo en memoria.
¿Promise.all vs allSettled?
all falla rápidamente con el primer error; allSettled para notificaciones por lotes donde el éxito parcial está bien.
¿Cómo afectan los temporizadores al apagado?
Borra los intervalos en SIGTERM; consulta los manuales de apagado elegante en la sección de operaciones de tiempo de ejecución.
¿Está bien el await de nivel superior?
Sí, en módulos ESM para la carga de configuración de inicio; no en la ruta por solicitud.
Relacionado
- Lista de verificación de reglas del proyecto Node - reglas 4-5
- Reglas de la API - reglas de tiempo de espera para manejadores
- Reglas de registro y observabilidad - registra métricas de retraso del bucle
Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.