
next-leak: ¿tiene tu app de Next.js una fuga de memoria?
npx next-leak .Averigua si tu app de Next.js tiene de verdad una fuga de memoria: cuánta, en qué ruta y de quién es la culpa.
Secciones
Qué responde
- No tienes una fuga: el pico es transitorio y se vacía en reposo. Es el caso más común.
- Algo se está llenando, no fugando: una caché acotada camino de su techo.
- La fuga está en tu código o en una dependencia, con el fichero fuente cuando es posible.
- Parece del propio framework, con un borrador de issue listo para abrir.
Empezar
Compila tu app con output: "standalone" y ejecuta esto desde su directorio:
next build npx next-leak .
Cómo mide
Cada ruta se mide en un proceso nuevo:
warm-up → forced GC → baseline snapshot → [load → idle → GC → sample] ×4 → snapshot
El veredicto sale de la forma de la curva tras el GC forzado, no de dónde está el heap: 40 MB y 400 MB no dicen nada por sí solos.
Veredictos
- stable
- Ningún crecimiento que esta ejecución pudiera detectar. No prueba que no haya fuga: el veredicto prefiere perder una fuga a inventarla.
- leak
- El heap retenido sigue creciendo en cada ciclo. Nombra al culpable cuando los source maps lo resuelven.
- saturating
- Cada ciclo creció, pero menos que el anterior: un almacén acotado que se queda sin claves nuevas.
- inconclusive
- La evidencia no decide. La ruta se vuelve a medir con el doble de ciclos.
- failed
- La ruta dio errores bajo carga: más de un 1% de respuestas que no son 2xx detiene la medición.
En unas 25 rutas sanas de apps en producción (PPR, MDX, Auth.js, Sentry, i18n) no dio ningún falso positivo.
Ejemplo real
/api/heap — reproducción de #95094, corregido en Next 16.3.0
| 1–3 | 28.7 · 40.3 · 59.0 |
|---|---|
| 4–6 | 75.8 · 75.9 · 101.2 |
| 7–9 | 101.2 · 139.0 · 139.0 |
| Pendiente | +4.70 MB/1000 req |
Retenedor
grown [object] Array 112.5 MB — TimeoutsManager#object[.resources] <- system / Context#object[.timeoutsManager] <- destroy#closure[.context] <- ResourceManager#object[.properties] <- IntervalsManager#object[.map]
Una ruta sana devuelve entre el 20 y el 30% de lo que crece. Esta no devuelve nada: esa es la forma de escalera.
Medir el build, no el servidor
npx next-leak build .
Un sitio grande puede quedarse sin heap mientras prerenderiza, antes de que exista un servidor que medir. Este comando ejecuta tu build sin modificarlo y muestrea la memoria residente de cada worker de generación estática. No necesita un build previo ni salida standalone.
| Next 16.3.3 | leak | Sin heap tras 1617 y 1525 de 2504 páginas, en dos ejecuciones. |
|---|---|---|
| Next 16.2.12 | Termina con 0.05 MB por página. |
Corregido en 16.3.5 con el mismo arreglo que #97938. Aún sin volver a medir aquí.
El proceso principal del build se informa, no se juzga. En esa reproducción bajó de 1.43 GB a 0.10 GB mientras los workers subían, así que sumarlos anularía el hallazgo.
--attribute además nombra qué retiene el worker. Es opcional y lento: el worker escribe todo su heap en disco.
Verificado con issues reales de Next.js
Estado de las issues revisado el 19 sept 2026.
| Issue | Qué es | Medido | Estado |
|---|---|---|---|
| #97938 | use cache / cacheComponents: AbortSignal.any composites never released | +705 KB per request on 16.3.3, flat on 16.2.6 | corregida en 16.3.5 |
| #84884 | axios + AbortSignal in middleware: a reference cycle through undici's Request finalizer | +17.02 MB/1000 req on 16.3.5 (Node 24.18), +4.62 (Node 24.21); flat with scope hoisting off | abierta · arreglo propuesto en nodejs/undici#5822 |
| #98707 | next dev: a route handler compiled after N pages costs ~16 MB × N | 851 MB on 16.3.0-canary.100, 2,354 MB on canary.101, 1,704 MB on 16.3.5 | abierta |
| #96533 | ISR revalidation holds RSC buffers between collections | 4–5 MB of arrayBuffers held vs 0.32 MB retained | abierta |
| #92287 | Cache Components: unbounded arrayBuffers under load | 37.5 MB of arrayBuffers held between collections, 37x what it retains (16.3.1) | abierta |
| #89091 | zlib retention on mid-stream aborts | +42.5 MB/1000 aborted req on 16.1.5; +0.03 on 16.3.1 | cerrada |
| #95094 | Middleware setTimeout ids retained by the sandbox | 112 MB retained; flat after the fix | corregida en 16.3.0 |
| #94890 | Router LRU cache doesn't count its keys | 26.7 → 71.9 MB | corregida en 16.3.0 |
| #94919 | Retention on client aborts | 39 → 139 MB | corregida en 16.3.0 |
En #95094 encontró la fuga, 28.7 → 138.9 MB en 8 ciclos. Con el workaround del hilo aplicado (clearTimeout(id)), misma app y mismos parámetros: 27.8 → 25.6 MB, plano.
Las corregidas siguen en la lista a propósito: muestran que las mediciones coincidieron con lo que resultaron ser los arreglos.
Alcance y límites
- El comando por defecto necesita App Router,
output: "standalone", Node 22 o posterior, y Linux o macOS. Pages Router, los builds sin standalone y Windows se rechazan con un mensaje claro. stableno prueba que no haya fuga. Ejecuta antes--self-check: planta una fuga de 8 KB por petición y demuestra que el arnés la ve donde lo estás ejecutando.- Cada proceso medido corre con un límite de heap de 512 MB, para que una fuga llegue al techo en minutos. Las apps con más memoria de trabajo necesitan
--max-old-space. - Una app de 60 rutas con los valores por defecto tarda horas. Acota con
--routesmientras iteras. - Nombrar el fichero requiere un build de Turbopack con source maps de servidor, lo normal desde Next 15. En builds de webpack los hallazgos quedan sin atribuir; la medición no depende de ello.
- Las rutas al límite pueden alternar entre
stableyleakde una ejecución a otra. Más ciclos lo resuelven. - La app se ejecuta con su entorno real: las rutas que llaman a servicios externos los llamarán bajo carga.
Enlaces
- github.com/xabierlameiro/next-leakCódigo fuente, README e issues
- npmjs.com/package/next-leakEl paquete en npm
- npmx.dev/package/next-leakTamaño de instalación, dependencias y descargas
- Cómo encontrar una fuga de memoria de Next.js en producciónEl artículo que explica el método
- next-coverageLa otra herramienta: ¿qué APIs de Next.js usa tu app?
- ¿Un veredicto incorrecto? Abre una issue