Dev server retains ~90 MB per edit of a Server Component and consumes memory without bounds
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.5k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/arunanshub/next-dev-oom-repro
To Reproduce
-
pnpm install -
NODE_OPTIONS="--max-old-space-size=3072" pnpm devNote that the heap cap is not required to see the leak. It does make the repro faster to observe.
-
Open
http://localhost:3000/once. -
Insert a line near the top of
app/page.tsx, save, wait about a second and reload the page. -
Repeat. Watch RSS of the
next-serverprocess.
You may also use the ./measure.sh script provided in the repo to observe the same effect.
It is important that you add lines at the top. React's cache key contains the line and column, so a top insert makes every key new. You should also reload the page because the growth happens during the re-render and not during compilation.
Current vs. Expected behavior
Current behavior
- RSS grows ~89 MB per edit over 12 edits and never falls (599 MB to 1666 MB).
- This memory growth is monotonic. Idling does not recover it. Only restarting the dev "fixes" it.
- With
--disable-source-mapsthe same 12 edits cost ~5 MB each (508 MB to 570 MB). - On a larger real file (a ~790-line MDX post) it went 1170 MB to 4872 MB in 17 seconds across 2 edits and the process died (when used with
--max-old-space-size=3072). Otherwise the RSS keeps growing until it gets OOMKilled or slows down the OS/VM.
However as mentioned below, Deno and Bun runtime do NOT suffer from this issue (more details below) and thus do not crash or slow down.
Expected behavior
- Memory should return to roughly its post-first-render level, or at worst grow to a bounded ceiling.
- Editing one file repeatedly returns the server to an equivalent state each time. I presume edit $n$ does not need anything that was retained from edit 1.
Provide environment information
Operating System:
Platform: linux
Arch: x64
Version: #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026
Available memory (MB): 15582
Available CPU cores: 16
Binaries:
Node: 26.6.0
npm: 11.18.0
Yarn: 1.22.22
pnpm: 11.25.0
Relevant Packages:
next: 16.4.0-canary.15
eslint-config-next: N/A
react: 19.2.8
react-dom: 19.2.8
typescript: 5.9.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Turbopack, React, Runtime
Which stage(s) are affected? (Select all that apply)
next dev (local)
Additional context
Measured (next 16.4.0-canary.15, node 26.6.0, linux x64)
RSS of the next-server process with one line inserted near the top of app/page.tsx per edit:
| run | after first render | after 12 edits | per edit |
|---|---|---|---|
next dev |
599 MB | 1666 MB | +89 MB |
next dev --disable-source-maps |
508 MB | 570 MB | +5 MB |
Observation is identical on 16.4.0-canary.14.
For the real world case, when editing the top of a ~790-line MDX post the dev server went from 1170 MB to 4872 MB in 17 seconds over 2 edits and then died.
Heap snapshot of the reproduction (canary.15, after 10 edits, RSS 1472 MB)
Total heap self size 1005 MB, of which:
| bytes | nodes | what |
|---|---|---|
| 448.3 MB | 6636 | fake-function eval sources (/* This module was rendered by a Server Component… */) |
| 208.5 MB | 6032 | the page's own source text, retained as sourcesContent |
| 107.7 MB | 6497 | that module's source-map mappings |
| 764.5 MB | 76% of the heap |
Two Maps hold it and they are same in size so they grow together:
Map @913547 table entries=13273 tableSelf=229416 <- context:generatedSourceMapCache (node source_map_cache)
Map @1126669 table entries=13273 tableSelf=229416 <- context:fakeFunctionCache (React flight client)
Retaining paths from the MDX heap snapshot that prompted this report:
string (eval'd script source, 531 KB)
<- internal:source code (script)
<- internal:shared code '_createMdxContent'
<- internal:12051 closure '_createMdxContent'
<- internal:table array (hash table)
<- context:fakeFunctionCache Map
string sourcesContent[0] (480 KB) / string mappings (132 KB)
<- property:sourcesContent / mappings Object (parsed map)
<- element:[0] <- property:sections <- property:data
<- internal:table array (hash table)
<- context:generatedSourceMapCache Map
Real cache keys showing why nothing is ever reused:
about://React/Server/file:///…/chunks/ssr/content_blog_…_0zxvqnp.js?3 <- node's key, counter
_createMdxContent-file:///…_0zxvqnp.js?id=…-<line>-<col>-…-Cache-n <- React's key, line/col
Mechanism
-
next devstarts the server with--enable-source-maps(next/dist/cli/next-dev.js:287). -
Server HMR modules are evaluated with
eval, under a sourceURL carrying?id=…— per Next's own comment quoted in step 4, and visible in the cache keys above. -
React's flight client rebuilds owner stacks.
buildFakeCallStackkeysfakeFunctionCachebyname-file-LINE-COL-encLine-encCol-flag-env(compiled/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js:3690, cache declared at :5338, written at :3726). -
On a miss it calls
findSourceMapURLDEV.getSourceMapURLFromTurbopackreturnsnullfor any
URL containing?(server/dev/hot-reloader-turbopack.js:272)Next's own comment there says why that matters: "
file:URLs with a query are eval'd server HMR modules carrying inline source maps". So Next falls back tomodule.findSourceMap,JSON.stringifys the whole payload includingsourcesContent, and base64-encodes it into adata:URL (server/lib/source-maps.js:157, built at :181-183). -
createFakeFunction(:3592) concatenates padding + that data URL +
//# sourceURL=about://React/<env>/<file>?<fakeFunctionIdx++>(:3675) and calls globaleval. -
Each miss leaves two permanent entries. React keeps the eval'd script source. Node parses the inline data URL and keeps the decoded map under a key that can never be hit again.
Inserting a line shifts the line and column of every element, so every frameKey is new per edit.
Why deno and bun do not hit this
| runtime | console.createTask |
findSourceMap(eval'd) |
|---|---|---|
node v26.6.0 --enable-source-maps |
function | FOUND |
| node v26.6.0 (no flag) | function | undefined |
| deno 2.9.6 | function | undefined |
| bun 1.4.1 | undefined | undefined |
bun has no console.createTask, so React's supportsCreateTask is false and createFakeFunction never runs. deno has it but does not cache generated source maps, so sourceMap is null and each eval'd source stays small.
--disable-source-maps mitigation
When running the dev server with pnpm dev --disable-source-maps, it removes node's issue. But React's cache still grows.
For the MDX case it grew ~40 KB per frame instead of ~1.05 MB because createFakeFunction pads its source with "\n".repeat(enclosingLine - 1) and V8 allocates a line-ends table per script. A snapshot of a flagged run on the MDX case showed 53,040 fake-function scripts and 956 MB of (script line ends).
Suggested fixes
-
Bound
fakeFunctionCache, or clear it per HMR generation. This alone caps the growth. -
The bigger cost is inlining a whole chunk's source map into every stack frame.
I do not know why a
data:URL is used rather than a URL the client fetches. I believe it's so that the browser can resolve it without a round trip so I am not confident proposing a fix here.
Related
- #77733 Same fake-script mechanism, reported as a debuggerproblem rather than a memory one.
- nodejs/node#65760 the
generatedSourceMapCacheissue filed upstream.
AI Disclosure
I used Claude to understand the problem, Nextjs' and React's internals and to produce the repro. Every observation was verified by me (the human).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run the linked next-dev-oom-repro with the stated pnpm commands and observe RSS, optionally using measure.sh. Read next/dist/cli/next-dev.js:287, server/dev/hot-reloader-turbopack.js:272, server/lib/source-maps.js:157 and the referenced React fakeFunctionCache entries; done means repeated edits no longer cause unbounded memory growth.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, node.js, react
- Domain
- backend, developer-experience, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100