
next-leak: find out whether your Next.js app leaks memory
npx next-leak .Find out whether your Next.js app actually leaks memory: how much, on which route, and whose fault it is.
Sections
What it answers
- You don't have a leak: the spike is transient and drains while idle. The most common case.
- Something is filling up, not leaking: a bounded cache on its way to its ceiling.
- The leak is in your code or in a dependency, named down to the source file when possible.
- It looks like framework internals, with an issue draft ready to file.
Start
Build your app with output: "standalone", then run this from its directory:
next build npx next-leak .
How it measures
Each route is measured in a fresh process:
warm-up → forced GC → baseline snapshot → [load → idle → GC → sample] ×4 → snapshot
The verdict comes from the shape of the curve after forced GC, not from where the heap sits: 40 MB and 400 MB say nothing on their own.
Verdicts
- stable
- No growth this run could detect. Not proof of absence: the verdict prefers missing a leak to inventing one.
- leak
- Retained heap keeps growing every cycle. It names the culprit when the source maps resolve it.
- saturating
- Every cycle grew, but by less than the one before: a bounded store running out of new keys.
- inconclusive
- The evidence does not decide. The route is measured again with twice the cycles.
- failed
- The route errored under load: more than 1% of non-2xx responses stops the measurement.
Across ~25 healthy routes on production apps (PPR, MDX, Auth.js, Sentry, i18n) it reported zero false positives.
Example run
/api/heap — reproduction for #95094, fixed in 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 |
| Slope | +4.70 MB/1000 req |
Retainer
grown [object] Array 112.5 MB — TimeoutsManager#object[.resources] <- system / Context#object[.timeoutsManager] <- destroy#closure[.context] <- ResourceManager#object[.properties] <- IntervalsManager#object[.map]
A healthy route gives back 20–30% of its growth. This one gives back nothing: that is the step shape.
Measure the build, not the server
npx next-leak build .
A large site can run out of heap while prerendering, before any server exists to measure. This command runs your build unmodified and samples the resident memory of each static-generation worker. It needs neither a previous build nor standalone output.
| Next 16.3.3 | leak | Out of heap after 1,617 and 1,525 of 2,504 pages, in two runs. |
|---|---|---|
| Next 16.2.12 | Finishes at 0.05 MB per page. |
Fixed in 16.3.5 by the same fix as #97938. Not re-measured here yet.
The build's own process is reported, not judged. On that reproduction it went from 1.43 GB down to 0.10 GB while the workers climbed, so adding the two would cancel the finding.
--attribute also names what the worker retains. It is opt-in and slow: the worker writes its whole heap to disk.
Verified against real Next.js issues
Issue states checked Sep 19, 2026.
| Issue | What it is | Measured | State |
|---|---|---|---|
| #97938 | use cache / cacheComponents: AbortSignal.any composites never released | +705 KB per request on 16.3.3, flat on 16.2.6 | fixed in 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 | open · fix proposed in 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 | open |
| #96533 | ISR revalidation holds RSC buffers between collections | 4–5 MB of arrayBuffers held vs 0.32 MB retained | open |
| #92287 | Cache Components: unbounded arrayBuffers under load | 37.5 MB of arrayBuffers held between collections, 37x what it retains (16.3.1) | open |
| #89091 | zlib retention on mid-stream aborts | +42.5 MB/1000 aborted req on 16.1.5; +0.03 on 16.3.1 | closed |
| #95094 | Middleware setTimeout ids retained by the sandbox | 112 MB retained; flat after the fix | fixed in 16.3.0 |
| #94890 | Router LRU cache doesn't count its keys | 26.7 → 71.9 MB | fixed in 16.3.0 |
| #94919 | Retention on client aborts | 39 → 139 MB | fixed in 16.3.0 |
On #95094 it found the leak, 28.7 → 138.9 MB across 8 cycles. With the workaround from the thread applied (clearTimeout(id)), same app and same parameters: 27.8 → 25.6 MB, flat.
The fixed ones stay on the list on purpose: they show that the measurements matched what the fixes turned out to be.
Scope and limits
- The default command needs the App Router,
output: "standalone", Node 22 or later, and Linux or macOS. Pages Router, non-standalone builds and Windows are rejected with a clear message. stableis not proof of absence. Run--self-checkfirst: it plants a leak of 8 KB per request and proves the harness sees it where you are running.- Each measured process runs under a 512 MB heap cap, so a leak reaches a ceiling in minutes. Apps with a larger working set need
--max-old-space. - A 60-route app under defaults takes hours. Narrow it with
--routeswhile you iterate. - Naming the file needs a Turbopack build with server source maps, the Next 15+ default. On webpack builds findings stay unattributed; the measurement does not depend on it.
- Borderline routes can flip between
stableandleakacross runs. More cycles resolve it. - The app runs with its real environment: routes that call external services will call them under load.
Links
- github.com/xabierlameiro/next-leakSource code, README and issues
- npmjs.com/package/next-leakThe package on npm
- npmx.dev/package/next-leakInstall size, dependency tree and downloads
- How to find a Next.js memory leak in productionThe post that explains the method
- next-coverageThe other tool: which Next.js APIs does your app use?
- Got a wrong verdict? Open an issue