Las API de Node.js que nunca liberan memoria eventualmente terminan con pods OOM-killed. Caza fugas con instantáneas de heap en Chrome DevTools, clinic heapprofiler bajo carga y revisión de código de listeners, cachés y cierres.
Tarjeta de receta de referencia rápida: lista para copiar y pegar.
# Empieza con inspect para instantáneasnode --inspect src/main.ts# O el perfilador de heap de Clinicnpm install -g clinicclinic heapprofiler -- node dist/main.js# ejercita la API, detén el proceso, abre el HTML generado
# Señal para instantánea de heap a disco (Node 24)kill -USR2 <pid> # si --heapsnapshot-near-heap-limit está configuradonode --heapsnapshot-signal=SIGUSR2 dist/main.js
Cuándo usarlo:
RSS crece linealmente durante días mientras el tráfico es plano
k8s reinicia pods con OOMKilled
Los tiempos de pausa del GC aumentan en las métricas
Después de añadir caché en memoria, bus de eventos o fanout de websocket
// FUGA: caché ilimitada indexada por solicitud - antipatrón para demostraciónconst leakCache = new Map<string, Buffer>();import express from "express";const app = express();app.post("/upload", express.raw({ type: "*/*", limit: "5mb" }), (req, res) => { const id = crypto.randomUUID(); leakCache.set(id, Buffer.from(req.body)); // nunca se desaloja res.json({ id });});// SOLUCIÓN: LRU con entradas máximas + TTLimport { LRUCache } from "lru-cache";const cache = new LRUCache<string, Buffer>({ max: 500, ttl: 1000 * 60 * 15,});app.post("/upload-fixed", express.raw({ type: "*/*", limit: "5mb" }), (req, res) => { const id = crypto.randomUUID(); cache.set(id, Buffer.from(req.body)); res.json({ id });});// DEBUG: registra el heap periódicamente en stagingsetInterval(() => { const m = process.memoryUsage(); console.log(JSON.stringify({ rss: m.rss, heapUsed: m.heapUsed, external: m.external }));}, 60_000).unref();
Flujo de trabajo de instantáneas de heap:
1. node --inspect dist/main.js2. Chrome DevTools -> Memoria -> Tomar instantánea de heap (línea base)3. Ejecuta la prueba de carga durante 5-10 minutos4. Toma una segunda instantánea5. Vista de comparación -> ordena por "Diferencia de tamaño" -> inspecciona las rutas de retención
Lo que esto demuestra:
Map ilimitado es una fuga clásica en las API de Node
LRU + TTL limita la memoria para los búferes de carga
El registro periódico de memoryUsage ayuda a correlacionar el crecimiento con las implementaciones
La comparación de DevTools encuentra objetos que crecieron entre instantáneas
// WeakMap para metadatos vinculados a la vida útil del objeto - las entradas se recolectan cuando la clave se recolectaconst meta = new WeakMap<object, { createdAt: number }>();
Conclusión de una sola instantánea - El heap parece "grande" normalmente. Solución: Compara dos instantáneas después de la carga.
Depuración en producción con --inspect abierto - Riesgo de seguridad. Solución: Instantánea en staging o usa el volcado de heap SIGUSR2 a S3 en un manual de procedimientos controlado.
Culpar a V8 por la caché de la aplicación - Las "fugas de Node" suelen ser Map de la aplicación. Solución: Audita las cachés después de cada característica.
No llamar a unref() en los temporizadores de depuración - El temporizador mantiene el bucle de eventos vivo en las pruebas. Solución:.unref() en los intervalos de diagnóstico.
Instantáneas enormes congelan el portátil - Las instantáneas de heap de 2 GB son lentas. Solución: Reproduce con una carga menor en staging primero.
heapUsed son objetos JavaScript de V8. rss es la memoria total del proceso, incluidos los búferes nativos. Rastrea ambos.
¿Cuánto tiempo entre instantáneas?
5-15 minutos bajo autocannon constante o tráfico de producción reproducido.
¿Las funciones flecha tienen fugas?
Tienen fugas si cierran sobre objetos grandes y se almacenan globalmente. El tipo de función no es el problema, la retención sí lo es.
¿Fugas de Socket.io?
Se acumulan espacios de nombres huérfanos y listeners por socket. Elimina los listeners al desconectar.
¿Memoria de Prisma?
El motor de consultas utiliza memoria nativa. El aumento de external con un heap plano puede ser un pool de conexiones o el motor; verifica el tamaño del pool.
¿Heap de worker threads?
Cada worker tiene un heap separado; agrega el RSS de todos los workers en los límites de k8s.
¿Puedo automatizar la instantánea en CI?
Prueba de regresión: ejecuta el script de carga, afirma que el delta de heapUsed está por debajo del umbral; es inestable pero detecta regresiones importantes.
¿clinic doctor vs heapprofiler?
doctor es para el bucle de eventos general + CPU; heapprofiler se enfoca en asignaciones para la caza de fugas.
¿Después de la verificación de la solución?
Misma prueba de carga, compara el delta de la instantánea; debería aplanarse en más de 30 minutos.
¿Fugas del pool de búferes?
Los Buffer retenidos de los analizadores agrupados se muestran en la instantánea como (string) o Buffer; verifica el middleware HTTP que retiene cuerpos.