nextjs - ✅(Solved) Fix Memory leak on async calls in dev server [1 pull requests, 12 comments, 7 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#85666Fetched 2026-04-08 02:14:36
View on GitHub
Comments
12
Participants
7
Timeline
34
Reactions
1
Author
Timeline (top)
commented ×12subscribed ×12mentioned ×7cross-referenced ×1

Fix Action

Fixed

PR fix notes

PR #91704: fix(swc): replace async generator in subscribe() with manual iterator to prevent pendingOperations Map overflow

Description (problem / solution / changelog)

What

Replaces the async function* createIterator() generator inside subscribe() in packages/next/src/build/swc/index.ts with a manually-implemented AsyncIterableIterator that returns Promise<IteratorResult<T>> directly from next().

Closes / related: #85666

Why

The crash

In dev mode (Turbopack), subscribe() is used to create one HMR subscription per compiled server chunk (via project.hmrEvents()). For a large application with many routes, dozens or hundreds of these subscriptions run simultaneously as infinite for await…of loops:

hot-reloader-turbopack.js
  └─ for await (const result of subscription) { … }
       └─ subscription = project.hmrEvents(chunkPath, HmrTarget.Server)
            └─ subscribe(true, …)  ← async generator lives here

React 19's dev-mode async profiler registers a Node.js AsyncHook (inside the compiled app-page-turbo.runtime.dev.js) that tracks every async resource in a pendingOperations Map, capturing a full stack trace per entry. The destroy hook is supposed to remove entries once async resources are garbage-collected, but under heavy HMR activity GC cannot keep up with creation.

This pendingOperations Map was independently identified as the leak source in #85666 (see this comment).

Why async generators make it worse

An async function* generator produces multiple async hook IDs per event:

Async resourceSource
AsyncGeneratorObjectThe generator itself — lives for the full subscription lifetime
Generator step-PromiseEach internal .next() call
Yielded inner-Promiseyield new Promise(…) on the "waiting" path
Promise.resolve() wrapperfor await…of unwrapping a yielded thenable

That is 3–4 async IDs per event, each stored in pendingOperations with a captured stack trace (≈1–5 KB per entry). With many active subscriptions producing frequent events the Map grows faster than V8's GC can drain it until V8 can no longer allocate memory for the Map's internal hash table:

RangeError: Map maximum size exceeded
    at Map.set (<anonymous>)
    at AsyncHook.init (…/app-page-turbo.runtime.dev.js:53:268561)
    at emitInitNative (node:internal/async_hooks:206:43)
    at promiseInitHookWithDestroyTracking (node:internal/async_hooks:336:3)
    at createIterator (…/next/dist/build/swc/index.js:512:31)
    at …/next/dist/server/dev/hot-reloader-turbopack.js:127:30

The server process exits after a few minutes of active development.

The fix

A manual async iterator returns exactly one Promise<IteratorResult<T>> from next() — no generator object, no step-Promise wrapping, no extra Promise.resolve() from for await…of. This reduces async-resource creation from O(events × 3–4) to O(events × 1), keeping pendingOperations bounded rather than growing without limit.

Semantics are identical: buffered events drain instantly via Promise.resolve(), and the "waiting" path resolves or rejects the returned Promise when emitResult fires. Task cleanup is moved into return() (previously it lived in the generator's finally block, which was only reachable after the generator resumed — a path that the overridden iterator.return bypassed anyway).

Testing

Start the dev server for a large Next.js / Turbopack app (many routes, active file editing) on Node.js v22+. Before this fix the server crashes with RangeError: Map maximum size exceeded after a few minutes. After this fix the server remains stable indefinitely under the same workload.

Verified on Node.js v24.11.1 + Next.js 16.2.0.

Changed files

  • packages/next/src/build/swc/index.ts (modified, +72/-27)

Code Example

import v8 from 'v8'

export async function GET(request: Request) {
    v8.setFlagsFromString('--trace_gc')

    for (; ;) { // This is an intentionally extreme example to make the issue easier to reproduce.
        await testFunc()
    }

    return Response.json({})
}

async function testFunc() {
    // NOP
}

---

const v8 = require('node:v8')
v8.setFlagsFromString('--trace_gc')

async function testFunc() {
    // NOP
}

(async () => {
    for (; ;) {
        await testFunc()
    }
})()

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000
  Available memory (MB): 65536
  Available CPU cores: 10
Binaries:
  Node: 24.11.0
  npm: 11.6.1
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.0.2-canary.3 // Latest available version is detected (16.0.2-canary.3).
  eslint-config-next: N/A
  react: 19.2.0
  react-dom: 19.2.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

The same issue occurs not only in 16.0.2-canary.3, but also in 16.0.1.
It also occurs in Node.js v22.21.1.
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/pontasan/nextjs_case20251101_2.git

To Reproduce

1.Start the server with npm run dev.

2.Open the following link in your browser: http://localhost:3000/test

3.After a while, the server crashes while GC logs are being printed.

Current vs. Expected behavior

It appears that a memory leak occurs only in the development server when calling async functions from route.ts. Running a large number of async functions in a loop results in a steady increase in heap usage over time. In contrast, there is no such issue in the production build.

I noticed that when running the development server for an extended period, the heap usage never decreases, which led me to conduct this experiment.

This example is intentionally extreme to make the issue easier to reproduce, but even in development mode, I believe it should be able to handle asynchronous function calls inside a loop just as it does in production.

import v8 from 'v8'

export async function GET(request: Request) {
    v8.setFlagsFromString('--trace_gc')

    for (; ;) { // This is an intentionally extreme example to make the issue easier to reproduce.
        await testFunc()
    }

    return Response.json({})
}

async function testFunc() {
    // NOP
}

The following image was taken right after the development server crashed. <img width="1118" height="766" alt="Image" src="https://github.com/user-attachments/assets/7fc91920-d14a-40b0-acd1-201eb5dac5bf" />

The issue does not reproduce in the production build or in the following Node.js code, and the heap remains stable.

const v8 = require('node:v8')
v8.setFlagsFromString('--trace_gc')

async function testFunc() {
    // NOP
}

(async () => {
    for (; ;) {
        await testFunc()
    }
})()

On the other hand, in the production build environment, the heap remains stable. <img width="982" height="743" alt="Image" src="https://github.com/user-attachments/assets/cf1cdfbe-1ad2-4626-85de-325a9fbe8372" />

Running the same test in Node.js also shows stable behavior. <img width="961" height="575" alt="Image" src="https://github.com/user-attachments/assets/5aae5bb4-b1c6-4881-b852-7c63e263db6b" />

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000
  Available memory (MB): 65536
  Available CPU cores: 10
Binaries:
  Node: 24.11.0
  npm: 11.6.1
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.0.2-canary.3 // Latest available version is detected (16.0.2-canary.3).
  eslint-config-next: N/A
  react: 19.2.0
  react-dom: 19.2.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

The same issue occurs not only in 16.0.2-canary.3, but also in 16.0.1.
It also occurs in Node.js v22.21.1.

Which area(s) are affected? (Select all that apply)

Route Handlers

Which stage(s) are affected? (Select all that apply)

next dev (local)

Additional context

Below is a video of the development server. The GC logs show a steady increase in memory consumption. https://drive.google.com/file/d/1_Z_2UWRbj1IWBtgIwCb7nQcfnZAdGwaw/view?usp=drive_link

Below is a video of the production build. The heap usage remains stable. https://drive.google.com/file/d/1byrJM7SMAw_vodNmji6zxkrpM6BrgG2d/view?usp=drive_link

Below is a video of the same process running in Node.js, where the heap also remains stable. https://drive.google.com/file/d/1PKZdfE_0eCR2QMG8aANzc7RxglOHfPQM/view?usp=drive_link

The test code can be found at the following link: https://github.com/pontasan/nextjs_case20251101_2.git

The Next.js example is located at: /app/test/route.ts The Node.js example is located at: /test.js

extent analysis

TL;DR

The most likely fix for the memory leak issue in the Next.js development server is to disable the --trace_gc flag or to use a production build.

Guidance

  • The issue seems to be related to the --trace_gc flag, which is enabled in the development server. Disabling this flag might resolve the memory leak issue.
  • The fact that the issue does not occur in the production build or in the Node.js code suggests that the problem is specific to the Next.js development server.
  • To verify the fix, run the development server without the --trace_gc flag and monitor the heap usage to see if it remains stable.
  • Another possible workaround is to use a production build for development, although this might not be desirable due to the differences in behavior between development and production builds.

Example

export async function GET(request: Request) {
    // Remove the --trace_gc flag
    // v8.setFlagsFromString('--trace_gc')

    for (; ;) { 
        await testFunc()
    }

    return Response.json({})
}

Notes

The root cause of the issue is not entirely clear, but it seems to be related to the interaction between the --trace_gc flag and the Next.js development server. Further investigation is needed to determine the exact cause of the problem.

Recommendation

Apply workaround: Disable the --trace_gc flag in the development server, as it seems to be the most likely cause of the memory leak issue. This flag is likely only needed for debugging purposes, and disabling it should not affect the normal functioning of the application.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING