vercel / vercel/next.js

Dev server retains ~90 MB per edit of a Server Component and consumes memory without bounds

Open
#98,221 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

React Runtime Turbopack
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
  1. pnpm install

  2. NODE_OPTIONS="--max-old-space-size=3072" pnpm dev

    Note that the heap cap is not required to see the leak. It does make the repro faster to observe.

  3. Open http://localhost:3000/ once.

  4. Insert a line near the top of app/page.tsx, save, wait about a second and reload the page.

  5. Repeat. Watch RSS of the next-server process.

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-maps the 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
  1. next dev starts the server with --enable-source-maps (next/dist/cli/next-dev.js:287).

  2. 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.

  3. React's flight client rebuilds owner stacks. buildFakeCallStack keys fakeFunctionCache by name-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).

  4. On a miss it calls findSourceMapURLDEV. getSourceMapURLFromTurbopack returns null for 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 to module.findSourceMap, JSON.stringifys the whole payload including sourcesContent, and base64-encodes it into a data: URL (server/lib/source-maps.js:157, built at :181-183).

  5. createFakeFunction (:3592) concatenates padding + that data URL +
    //# sourceURL=about://React/<env>/<file>?<fakeFunctionIdx++> (:3675) and calls global eval.

  6. 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 generatedSourceMapCache issue 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.