Title: Node-API addons with normal finalizers keep GBs of dead external memory on V8 >= 14.8 (v8-canary) · Issue #66859 · nodejs/node · GitHub
Open Graph Title: Node-API addons with normal finalizers keep GBs of dead external memory on V8 >= 14.8 (v8-canary) · Issue #66859 · nodejs/node
X Title: Node-API addons with normal finalizers keep GBs of dead external memory on V8 >= 14.8 (v8-canary) · Issue #66859 · nodejs/node
Description: Version v27.0.0-v8-canary2026101812b4708d0e (V8 15.8.21). Not reproducible on v26.12.0 (V8 14.6.202.34). Platform Darwin 24.6.0 arm64 (macOS) Subsystem node-api (root cause in V8 heap) What steps will reproduce the bug? This is a Node-AP...
Open Graph Description: Version v27.0.0-v8-canary2026101812b4708d0e (V8 15.8.21). Not reproducible on v26.12.0 (V8 14.6.202.34). Platform Darwin 24.6.0 arm64 (macOS) Subsystem node-api (root cause in V8 heap) What steps w...
X Description: Version v27.0.0-v8-canary2026101812b4708d0e (V8 15.8.21). Not reproducible on v26.12.0 (V8 14.6.202.34). Platform Darwin 24.6.0 arm64 (macOS) Subsystem node-api (root cause in V8 heap) What steps w...
Opengraph URL: https://github.com/nodejs/node/issues/66859
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Node-API addons with normal finalizers keep GBs of dead external memory on V8 >= 14.8 (v8-canary)","articleBody":"### Version\n\nv27.0.0-v8-canary2026101812b4708d0e (V8 15.8.21). Not reproducible on v26.12.0 (V8 14.6.202.34).\n\n### Platform\n\n```text\nDarwin 24.6.0 arm64 (macOS)\n```\n\n### Subsystem\n\nnode-api (root cause in V8 heap)\n\n### What steps will reproduce the bug?\n\nThis is a Node-API addon that wraps 1 MiB of native memory per object, reports it with `napi_adjust_external_memory`, and releases it in a normal (non-basic) `napi_wrap` finalizer. The script drops every object immediately and yields to the event loop every 100 allocations.\n\n`addon.c`:\n\n```c\n#include \u003cnode_api.h>\n#include \u003cstdlib.h>\n\n#define SIZE (1 \u003c\u003c 20)\nstatic int64_t live = 0;\n\nstatic void finalize(napi_env env, void* data, void* hint) {\n int64_t ignored;\n free(data);\n live--;\n napi_adjust_external_memory(env, -SIZE, &ignored);\n}\n\nstatic napi_value Make(napi_env env, napi_callback_info info) {\n napi_value obj;\n int64_t ignored;\n void* mem = malloc(SIZE);\n live++;\n napi_create_object(env, &obj);\n napi_adjust_external_memory(env, SIZE, &ignored);\n napi_wrap(env, obj, mem, finalize, NULL, NULL);\n return obj;\n}\n\nstatic napi_value Live(napi_env env, napi_callback_info info) {\n napi_value result;\n napi_create_int64(env, live, &result);\n return result;\n}\n\nstatic napi_value Init(napi_env env, napi_value exports) {\n napi_value fn;\n napi_create_function(env, \"make\", NAPI_AUTO_LENGTH, Make, NULL, &fn);\n napi_set_named_property(env, exports, \"make\", fn);\n napi_create_function(env, \"live\", NAPI_AUTO_LENGTH, Live, NULL, &fn);\n napi_set_named_property(env, exports, \"live\", fn);\n return exports;\n}\n\nNAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n```\n\n`binding.gyp`:\n\n```json\n{ \"targets\": [ { \"target_name\": \"addon\", \"sources\": [\"addon.c\"] } ] }\n```\n\n`run.js`:\n\n```js\nconst { make, live } = require(\"./build/Release/addon.node\");\n(async () => {\n let peak = 0;\n for (let i = 0; i \u003c 20000; i++) {\n make();\n if (i % 100 === 99) {\n peak = Math.max(peak, live());\n await new Promise((r) => setImmediate(r));\n }\n }\n console.log(`${process.version} (V8 ${process.versions.v8}): peak live = ${peak} MiB`);\n})();\n```\n\n```sh\nnpx node-gyp rebuild --nodedir=/path/to/node\nnode run.js\n```\n\n### How often does it reproduce? Is there a required condition?\n\nAlways on the v8-canary build. The condition is that native memory reported with `napi_adjust_external_memory` is released from a normal finalizer, i.e. after the GC that found the object dead.\n\n### What is the expected behavior? Why is that the expected behavior?\n\nThe amount of dead native memory waiting for collection should stay bounded, as it does on v26.12.0:\n\n```text\nv26.12.0 (V8 14.6.202.34-node.39): peak live = 136 MiB\n```\n\n### What do you see instead?\n\n```text\nv27.0.0-v8-canary2026101812b4708d0e (V8 15.8.21-node.7): peak live = 5660 MiB\n```\n\nGCs back off geometrically. `--trace-gc --trace-gc-verbose --trace-gc-nvp` shows the global allocation limit tracking the garbage, at 64, 117, 204, 272, 461, … MB.\n\nUsing a basic finalizer (`NAPI_EXPERIMENTAL`, which Node calls from the first-pass weak callback) keeps the peak at about 1 MB.\n\n### Additional information\n\nThe root cause is in V8, filed as https://issues.chromium.org/issues/574627777, which includes a stock-V8-API repro.\n\nSince V8 14.8 (6a5039d9db9, `external_memory_accounted_in_global_limit` on by default), V8 computes the global allocation limit inside the GC pause from external memory that still includes the objects that GC just found dead. Memory released after the pause never lowers that limit. That covers second-pass callbacks and the normal Node-API finalizers queued by `napi_env__::InvokeFinalizerFromGC`.\n\nNormal finalizers are the Node-API default, so native addons that hold large buffers will be affected once Node ships a V8 with this scheme. On v26 the same behavior can be previewed with `--external-memory-accounted-in-global-limit`. 14.6's implementation of that flag differs, though, so the canary is the faithful repro.\n\nThe scheme is also discussed in #61967 as the expected fix for the Buffer regression there.\n","author":{"url":"https://github.com/edusperoni","@type":"Person","name":"edusperoni"},"datePublished":"2026-10-19T17:49:09Z","url":"https://github.com/nodejs/node/issues/66859"}
| 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:8b20ff01-63f3-3cf3-12be-8bea1849d23c |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | B412:F5278:AF9538:E891D8:6AED34EC |
| html-safe-nonce | 8431b41b4a9136b32dac02b66ed0b96185143a2228b2121af095785936e8a70b |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCNDEyOkY1Mjc4OkFGOTUzODpFODkxRDg6NkFFRDM0RUMiLCJ2aXNpdG9yX2lkIjoiNjExMTIzNDI4NjM4MDk4NzYyOCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 0a4831915749276bf83543c677a3ede23de23b93bac0d33c855ae550213469d8 |
| hovercard-subject-tag | issue:5928746473 |
| 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/66859/issue_layout |
| twitter:image | https://opengraph.githubassets.com/46012aa6bc1ebdec46562a54c28f8d7ab6a25e2c189314374b3061c312d65bc2/nodejs/node/issues/66859 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/46012aa6bc1ebdec46562a54c28f8d7ab6a25e2c189314374b3061c312d65bc2/nodejs/node/issues/66859 |
| og:image:alt | Version v27.0.0-v8-canary2026101812b4708d0e (V8 15.8.21). Not reproducible on v26.12.0 (V8 14.6.202.34). Platform Darwin 24.6.0 arm64 (macOS) Subsystem node-api (root cause in V8 heap) What steps w... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | edusperoni |
| hostname | github.com |
| expected-hostname | github.com |
| None | e51d0b7b5ab34b1688c05b54c3ed17b8658b2fa9bca973a9c740683ccb12cf4a |
| 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 | 59ebb7b901cc154ecc179a44f266fcf69f343b30 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width