Title: v24/v26: idle memory not released for ~100 s after load (V8 memory reducer regression) · Issue #66564 · nodejs/node · GitHub
Open Graph Title: v24/v26: idle memory not released for ~100 s after load (V8 memory reducer regression) · Issue #66564 · nodejs/node
X Title: v24/v26: idle memory not released for ~100 s after load (V8 memory reducer regression) · Issue #66564 · nodejs/node
Description: Version v24.21.0 (V8 13.6.233.17), v26.10.0 (V8 14.6.202.34). Not reproducible on v22.23.3 (V8 12.4). Platform Linux arm64 (Docker `node:*-bookworm-slim`); first noticed on x86_64 in production. Subsystem v8 What steps will reproduce the...
Open Graph Description: Version v24.21.0 (V8 13.6.233.17), v26.10.0 (V8 14.6.202.34). Not reproducible on v22.23.3 (V8 12.4). Platform Linux arm64 (Docker `node:*-bookworm-slim`); first noticed on x86_64 in production. Su...
X Description: Version v24.21.0 (V8 13.6.233.17), v26.10.0 (V8 14.6.202.34). Not reproducible on v22.23.3 (V8 12.4). Platform Linux arm64 (Docker `node:*-bookworm-slim`); first noticed on x86_64 in production. Su...
Opengraph URL: https://github.com/nodejs/node/issues/66564
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"v24/v26: idle memory not released for ~100 s after load (V8 memory reducer regression)","articleBody":"### Version\n\nv24.21.0 (V8 13.6.233.17), v26.10.0 (V8 14.6.202.34). Not reproducible on v22.23.3 (V8 12.4).\n\n### Platform\n\n```text\nLinux arm64 (Docker `node:*-bookworm-slim`); first noticed on x86_64 in production.\n```\n\n### Subsystem\n\nv8\n\n### What steps will reproduce the bug?\n\nSave as `repro.js`, run `node --trace-gc --trace-memory-reducer --trace-mutator-utilization repro.js`.\nAllocates ~1 GB of short-lived objects for 5 s, drops them, idles 150 s with a 2 s `setInterval`\nlogging `process.memoryUsage()`.\n\n```js\n'use strict';\n// node --trace-gc --trace-memory-reducer --trace-mutator-utilization repro-min.js\n// 5 s allocation burst, drop everything, idle 150 s logging process.memoryUsage() every 2 s.\nconst t0 = Date.now();\nconst mb = (n) => (n / 1048576).toFixed(1);\nfunction log(tag) {\n const m = process.memoryUsage();\n console.log(`[${((Date.now() - t0) / 1000).toFixed(1)}s] ${tag} rss=${mb(m.rss)}MB heapTotal=${mb(m.heapTotal)}MB heapUsed=${mb(m.heapUsed)}MB`);\n}\nfunction makeChunk(i) { // ~2.5 MB of objects and strings\n const arr = new Array(20000);\n for (let j = 0; j \u003c arr.length; j++) {\n arr[j] = { id: i * 100000 + j, name: 'item-' + i + '-' + j, tags: [j, j + 1, 'x' + j], payload: 'p'.repeat(32) + j };\n }\n return arr;\n}\nlog(`start node=${process.version} v8=${process.versions.v8}`);\nlet window = [], i = 0;\nconst burstEnd = Date.now() + 5000;\n(function burst() { // sliding window of ~48 live chunks; run in 50 ms slices so GC tasks can interleave\n const sliceEnd = Date.now() + 50;\n while (Date.now() \u003c sliceEnd) { window.push(makeChunk(i++)); if (window.length > 48) window.shift(); }\n if (Date.now() \u003c burstEnd) return setImmediate(burst);\n log(`burst done chunks=${i}`);\n window = null; // everything is now garbage\n const idleStart = Date.now();\n const iv = setInterval(() => {\n log('idle');\n if (Date.now() - idleStart >= 150000) { clearInterval(iv); log('end'); process.exit(0); }\n }, 2000);\n})();\n```\n\n(Same script plus idle-mode variants, raw traces, flag experiments and a patched/control Node 24 build:\nhttps://github.com/acburdine/node-memory-reducer-repro)\n\n### How often does it reproduce? Is there a required condition?\n\nEvery run.\n\n### What is the expected behavior? Why is that the expected behavior?\n\nAs on v22: a few reducer ticks after allocation stops, `Memory reducer: low alloc` → `Mark-Compact (reduce)`, heap and RSS drop.\n\n### What do you see instead?\n\n```\n16283 ms: Embedder mutator utilization = 0.000 (mutator_speed=0, gc_speed=1)\n16283 ms: Memory reducer: high alloc, foreground\n16283 ms: Memory reducer: waiting for 8000 ms\n... identical every 8 s ...\n105424 ms: Memory reducer: started GC #1 \u003c- watchdog, 100 s after the last major GC\n105559 ms: Mark-Compact (reduce) 681.6 (777.1) -> 4.3 (20.6) MB\n```\n\nTime from end of burst to first reduce GC:\n\n| idle mode | v22.23.3 | v24.21.0 | v26.10.0 | v24.21.0 + patch |\n|---|---|---|---|---|\n| 2 s timer | 44 s | 100 s (watchdog) | 97 s (watchdog) | 11 s |\n| + small garbage every 100 ms | 36 s | none in 150 s; RSS stayed ~700 MB | 122 s (watchdog) | 11 s |\n| no callbacks | 44 s | 100 s | 98 s | 11 s |\n\nIn the second row an ordinary major GC during idle resets the watchdog clock but leaves the freed\npages pooled, so RSS doesn't come down at all within the run. In Ghost this shows as the full\npost-load heap and ~2× RSS being held for ~100 s after every traffic burst on 24/26, vs ~10 s on 22.\n\n### Additional information\n\n### Disclaimer\nThe investigation and first drafts were done with help from an AI coding assistant; the source reading, repro runs and the numbers above were checked by hand against the V8 sources at the three Node tags. Also, the noticed effects of this issue surfaced empirically when benchmarking a Node 24/26 update for [Ghost](https://github.com/TryGhost/Ghost), which is currently on Node 22.\n\n### Root cause\nV8 CL https://crrev.com/c/5935313 (V8 13.1) replaced the ring-buffer allocation-throughput tracker (floored at 1 B/ms) with `SmoothedBytesAndDuration` (no floor). A process whose sampled cppgc allocation throughput is exactly 0 — any Node process not using `node:vm`, the core path that allocates on cppgc — hits `ComputeMutatorUtilizationImpl()`'s `mutator_speed == 0 → 0.0` return, so `Heap::HasLowEmbedderAllocationRate()` and therefore `Heap::HasLowAllocationRate()` are never true.\n\nUpstream bug: https://issues.chromium.org/issues/570738027. Details, control build and traces in the repo.\n\n### Workarounds\n\n- No `--memory-reducer*` / `--gc-memory-reducer*` flag changes this.\n- `--optimize-for-size` and `--memory-saver-mode` make the reducer run (`MemorySaverModeEnabled()`), at the cost of every incremental GC during load becoming a reduce GC (18 of them in the 5 s burst).\n- Compiling a trivial `vm.Script` periodically (a cppgc allocation) makes the embedder term pass; one at startup wears off after a long idle, one every 30 s survived a second burst in the repro. Works, but apps shouldn't have to.\n\n### Patch\n\nThe following patch can be applied to the V8 source code to fix the regression (should apply cleanly in both Node 24 and current main)\n\n```patch\n--- a/deps/v8/src/heap/heap.cc\n+++ b/deps/v8/src/heap/heap.cc\n@@ -3716,8 +3716,15 @@ bool Heap::HasLowOldGenerationAllocationRate() {\n }\n \n bool Heap::HasLowEmbedderAllocationRate() {\n+ const double embedder_allocation_rate =\n+ tracer()->EmbedderAllocationThroughputInBytesPerMillisecond();\n+ // A sampled CppHeap allocation throughput of exactly 0 is the lowest\n+ // possible allocation rate. ComputeMutatorUtilization() would otherwise\n+ // treat a mutator speed of 0 as maximal allocation and the memory reducer\n+ // could not start through its low-allocation condition.\n+ if (embedder_allocation_rate == 0) return true;\n double mu = ComputeMutatorUtilization(\n- \"Embedder\", tracer()->EmbedderAllocationThroughputInBytesPerMillisecond(),\n+ \"Embedder\", embedder_allocation_rate,\n tracer()->EmbedderSpeedInBytesPerMillisecond());\n const double kHighMutatorUtilization = 0.993;\n return mu > kHighMutatorUtilization;\n```\n\nWould a floating patch on v24.x/v26.x be acceptable ahead of the upstream fix? Happy to submit the patch as a PR if that's desired 😄","author":{"url":"https://github.com/acburdine","@type":"Person","name":"acburdine"},"datePublished":"2026-10-06T23:54:19Z","url":"https://github.com/nodejs/node/issues/66564"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:d628fc0f-2e5a-010e-17b4-7c9730262f32 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | E4F4:12D2BD:76238A:9F9978:6AED29E6 |
| html-safe-nonce | 6c33559006e25514a47165c69b51690a4badcd58c4f96c4265c0b8e67e1b6f73 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFNEY0OjEyRDJCRDo3NjIzOEE6OUY5OTc4OjZBRUQyOUU2IiwidmlzaXRvcl9pZCI6IjIxMTkwODgyNjM3OTA0MDYxMTgiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | e455a10a3ebacfe6c8be618522f7c01e579bb495e4a430606fa737a86f45648a |
| hovercard-subject-tag | issue:5737003258 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/66564/issue_layout |
| twitter:image | https://opengraph.githubassets.com/f2fe8c658938f53dd6dfba2ba0e7e1bffea15ec2ca9b81843a436f23c09df311/nodejs/node/issues/66564 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/f2fe8c658938f53dd6dfba2ba0e7e1bffea15ec2ca9b81843a436f23c09df311/nodejs/node/issues/66564 |
| og:image:alt | Version v24.21.0 (V8 13.6.233.17), v26.10.0 (V8 14.6.202.34). Not reproducible on v22.23.3 (V8 12.4). Platform Linux arm64 (Docker `node:*-bookworm-slim`); first noticed on x86_64 in production. Su... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | acburdine |
| hostname | github.com |
| expected-hostname | github.com |
| None | 9d95365dda0576efe9b71f45807595e396bd0ca9286fba5367a05048e6ddf8f2 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 98b41fa4b5635ceb45a4c54320fced73817ff290 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width