Home / Articles / Node's Source Map Cache Is a Silent Memory Leak in Dev Mode

This article is published in English.

Node's Source Map Cache Is a Silent Memory Leak in Dev Mode

Learn why enabling --enable-source-maps or NODE_V8_COVERAGE can cause unbounded heap growth from repeated eval calls, and how to diagnose and mitigate it today.

1073 words

Your Node process looks perfectly healthy right after startup, then slowly balloons in memory as you keep editing code. If that process was launched with --enable-source-maps (or with NODE_V8_COVERAGE set), and something in your stack keeps calling eval with a fresh //# sourceURL each time, you have likely found the culprit: a strong Source Map cache that accumulates generated entries and never releases them. The heap keeps growing. You restart the process. It grows again.

You try forcing garbage collection. You clear out every reference you can think of. The heap keeps climbing anyway.

The Growth Pattern

Picture a dev server left running for a while, or really any long-lived Node process that regenerates synthetic stack traces or "owner frames" tagged with unique source URLs. Both RSS and heapUsed trend upward the whole time. Restarting the process resets the curve, but running the identical workload without the source-map flag keeps memory flat.

On the public Node.js issue tracking this behavior (nodejs/node#65760), a minimal reproduction evaluates roughly 90 bytes of source code against a single external source map, changing only the sourceURL on each iteration. After running forced garbage collection:

Evals | with --enable-source-maps | no flag
0     | 5 MB                       | 5 MB
400   | 170 MB                     | 5 MB
800   | 334 MB                     | 6 MB
1200  | 499 MB                     | 6 MB

That works out to roughly 415 KB retained per eval call, with no upper bound in sight.

If you work with the Next.js App Router, this often surfaces as next dev gaining tens of megabytes with every file edit. The React Server Components owner-stack mechanism runs eval once per stack frame, tagging each call with something like //# sourceURL=about://React/…?<counter++> alongside a large inlined source map. Because that counter increments on every call, each cache key is unique, so nothing is ever reused. The corresponding Next.js discussion thread is vercel/next.js#98221 — think of it as the place where the symptom gets triggered, not as an independent cause.

Why the Cache Won't Let Go

The --enable-source-maps flag instructs Node to cache Source Maps so that runtime stack traces can be translated back to your original source files (see the CLI documentation). Regular module sources go through a weakly-keyed cache, so the garbage collector can reclaim them once they're no longer referenced. Generated sources, however — the ones handled by the isGeneratedSource branch — end up in generatedSourceMapCache, a plain, strongly-referenced Map defined in lib/internal/source_map/source_map_cache.js that is never pruned.

The code comment around that cache assumes there will only be a handful of generated sources over a process's lifetime. Hot Module Replacement and owner-stack regeneration break that assumption completely. Every distinct sourceURL becomes a permanent key, nothing ever deletes old entries, and the full parsed map for each one stays resident in memory.

Setting NODE_V8_COVERAGE routes through this exact same cache path, so you'll see the same unbounded growth whenever your generated eval keys keep changing.

There is already an open pull request upstream (#65761) that caps the generated-sources cache using a byte-budget LRU strategy — 32 MiB in the most recent revision — and refreshes entries on read so that maps for still-active generated functions aren't evicted prematurely. As of now, that PR remains open and marked needs-ci. It hasn't been merged, and it isn't part of any released Node build, so don't assume your current LTS version already includes this fix.

Confirming the Diagnosis

  1. Verify that your long-running process was started with --enable-source-maps or has NODE_V8_COVERAGE set, and that something is repeatedly eval-ing code with a new //# sourceURL each time — this could be HMR, RSC owner stacks, or custom codegen.
  2. Sample process.memoryUsage().heapUsed after running a forced-GC loop, either in a standalone reproduction started with node --expose-gc, or by taking a heap snapshot through the inspector on your live process.
  3. Run the same workload with the flag removed. The telltale sign from #65760 is memory staying flat without the flag and steadily climbing with it enabled.
  4. Optionally, capture a heap snapshot and search it for generatedSourceMapCache or context:generatedSourceMapCache. Reporters on the issue found thousands of retained entries holding onto more than a gigabyte of combined sourcesContent and mappings data during a real next dev session.

Bumping up --max-old-space-size is not a fix — it just postpones the eventual out-of-memory crash.

What to Do Right Now

Pick the approach that fits your setup:

  1. For Next.js projects, act immediately: run next dev --disable-source-maps. Reporters on #65760 saw growth flatten out substantially — roughly +6 MB per edit compared to about +89 MB with source maps enabled, based on their measurements. You lose some stack-trace readability, but your machine stops running out of memory.
  2. For other long-running Node tools: remove --enable-source-maps or unset NODE_V8_COVERAGE on any hot-reloading server until mapped stack traces are actually necessary. Reserve source mapping for short-lived debugging sessions instead.
  3. Track the upstream fix: follow both nodejs/node#65760 and pull request #65761. Once a Node release explicitly mentions the bounded generated-sources cache, it's safe to upgrade. Until then, treat any numbers from other reporters as measurements from their specific setup, not a guarantee that your application will behave identically.
  4. Skip the "just raise the heap limit" advice. The underlying cache is strong and unbounded under this exact combination of flags and usage pattern. A bigger heap ceiling only buys you a little more time before the crash.

The Takeaway

Your dev server isn't randomly consuming memory for no reason. Combining --enable-source-maps with repeated evals that use unique sourceURL values fills up a strongly-referenced generatedSourceMapCache that never releases its entries. Regular module source maps can be garbage collected; generated ones cannot, at least not until #65761 lands in a release. Turn off source maps on hot-reload processes today, and plan to upgrade once the bounded cache ships.

So the next time you notice RSS creeping upward while you're just editing files, and forced garbage collection does nothing to stop it, check whether that flag is the reason.