Un memory leak en Node.js puede tumbar tu servidor sin avisar. Aquí aprendes a detectarlo, medirlo con heap snapshots y corregirlo antes de que el proceso crashee por falta de memoria, usando herramientas como NSolid y Chrome DevTools.
Qué es un memory leak en Node.js y por qué importa
Un memory leak ocurre cuando un programa acumula memoria sin liberarla. Suena inofensivo, pero el heap de Node no es infinito: por defecto trabaja con 2 GB, y si tu aplicación supera ese límite, el proceso crashea.
Esto puede pasar por errores en el código o por comportamientos inesperados del programa. Lo grave es que entre más tráfico recibe tu servicio, más rápido se agota la memoria.
¿Cuál es el límite de memoria por defecto en Node.js? El heap de Node arranca con un máximo de 2 GB. Si tu proceso supera ese tope, termina con un error por falta de memoria.
Cómo reproducir un memory leak con Express y EventEmitter
Para entenderlo en acción, montamos un escenario realista con Express y un EventEmitter personalizado [01:30]. La idea es simular un endpoint que filtra memoria con cada request.
Cómo se construye el ejemplo paso a paso
El montaje es sencillo y refleja patrones comunes en aplicaciones reales:
- Importas Express y
EventEmitter desde node:events.
- Creas una clase
LeakyEmitter que extiende EventEmitter.
- Defines un arreglo vacío
listeners que va a guardar funciones.
- Dentro del endpoint, creas un
listener que imprime un evento usando req.url.
- Empujas ese listener al arreglo con
listeners.push(listener).
- Suscribes el listener al evento
data con emitter.on('data', listener).
- Emites el evento con
emitter.emit('data', { ... }).
Aquí ya hay una bomba de tiempo. Cada request agrega un listener al arreglo y nunca lo libera.
Cómo medir la memoria desde el propio proceso
Node expone process.memoryUsage(), una función que devuelve métricas clave [03:45]. Las dos más útiles para detectar fugas son:
- heapUsed: la memoria que tu programa está usando en este momento.
- heapTotal: la memoria que el proceso tiene asignada actualmente.
Un setInterval que imprime ambos valores en megabytes con .toFixed(2) te da un monitoreo en vivo. El heapTotal varía según lo que el programa requiera, así que verlo crecer sin parar es una señal clara de fuga.
Cómo se ve el memory leak bajo carga real
Con el servidor corriendo en NSolid, la app arranca usando alrededor de 8 MB de heap total y unos 7 MB de heap used. Todo normal hasta ahí.
Entonces entra Autocannon disparando carga durante 20 segundos. Después de 3.000 requests, el panorama cambia: el heap total salta a 63 MB y el heap used a 29 MB [05:50]. Repites la carga y la memoria sube de nuevo, sin volver al punto inicial.
¿Cómo sé si mi app tiene un memory leak? Si después de cada pico de tráfico el heap used no regresa al valor inicial y crece de forma sostenida, hay una fuga. El garbage collector recupera algo de memoria, pero no toda.
Qué métricas vigilar en NSolid
Más allá del heap, hay dos métricas del garbage collector que delatan el problema:
- GC duration: la duración media del recolector de basura.
- GC count: cuántas veces se dispara el recolector.
Cuando el GC corre cada vez más seguido pero el heap sigue subiendo, sabes que el recolector está peleando contra una referencia que no debería existir.
Por qué se filtra la memoria en este código
El culpable es una sola línea. Dentro del listener, al referenciar req.url, estás atando ese listener al objeto request completo de Express.
Normalmente, cuando el endpoint termina, el request se libera. Pero como guardas el listener en un arreglo global, el listener mantiene viva la referencia al request. El recolector ve ese objeto como aún en uso y no lo libera.
La solución es trivial: en lugar de referenciar req.url dentro del listener, guardas el valor en una variable string y usas esa. El objeto request queda libre para ser recolectado.
Cómo analizar la memoria con heap snapshots
Un heap snapshot es una fotografía del estado interno de la memoria del proceso [09:10]. Desde NSolid puedes capturar uno por proceso individual y descargarlo como un archivo.
Cómo leer un heap snapshot en Chrome DevTools
Ese archivo lo abres en Chrome DevTools, en la pestaña Memory. Ahí cargas el snapshot y obtienes una vista detallada de qué objetos están reteniendo memoria y dónde viven.
Vas a ver cosas como object shape, descriptor y muchas referencias a Express, porque cada request retenido arrastra sus propios objetos internos. La columna retained size te dice cuánta memoria mantiene vivo cada objeto.
Cómo comparar snapshots para encontrar la fuga
La técnica más efectiva es tomar varios snapshots en momentos distintos y compararlos. Las diferencias te muestran qué objetos crecieron entre captura y captura, lo que apunta directamente a la zona del código donde la memoria se está acumulando.
En este caso ya sabíamos dónde estaba el problema, pero en aplicaciones reales esa comparación es tu mejor pista.
Si has enfrentado un memory leak en producción, cuéntame en los comentarios cómo lo detectaste y qué herramienta te dio la pista clave.