Skip to content

module: add clearCache for CJS and ESM - #61767

Open
anonrig wants to merge 1 commit into
nodejs:mainfrom
anonrig:yagiz/node-module-clear-cache
Open

anonrig wants to merge 1 commit into
nodejs:mainfrom
anonrig:yagiz/node-module-clear-cache

Conversation

@anonrig

@anonrig anonrig commented Feb 10, 2026

Copy link
Copy Markdown
Member

Introduce Module.clearCache() to invalidate CommonJS and ESM module caches with optional resolution context, enabling HMR-like reloads. Document the API and add tests/fixtures to cover cache invalidation behavior.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added esm Issues and PRs related to the ECMAScript Modules implementation. module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Feb 10, 2026
@anonrig
anonrig force-pushed the yagiz/node-module-clear-cache branch 2 times, most recently from 90303e6 to 1d0accc Compare February 10, 2026 21:25
@anonrig anonrig added semver-minor PRs that contain new features and should be released in the next minor version. notable-change PRs with changes that should be highlighted in changelogs. labels Feb 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The notable-change PRs with changes that should be highlighted in changelogs. label has been added by @anonrig.

Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. Otherwise, the commit will be placed in the Other Notable Changes section.

@mcollina

Copy link
Copy Markdown
Member

I’m relatively +1 on having this in Node.js, but I recall having a a lot of discussions about this @GeoffreyBooth and @nodejs/loaders teams about this, and it would massively break the spec, expectations, and invariants regarding ESM.

(Note, this is what people have been asking us to add for a long time).

My personal objection to this API is that it would inadvertently leak memory at every turn, so while this sounds good in theory, in practice it would significantly backfire in long-running scenarios. An option could be to expose it only behind a flag, putting the user in charge of choosing this behavior.

Every single scenario where I saw HMR in Node.js ends up in memory leaks. This is the reason why I had so much interest and hopes for ShadowRealm.

@benjamingr benjamingr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still +1 on the feature from a user usability point of view. Code lgtm.

@benjamingr

Copy link
Copy Markdown
Member

Every single scenario where I saw HMR in Node.js ends up in memory leaks. This is the reason why I had so much interest and hopes for ShadowRealm.

We're giving users a tool, it may be seen as a footgun by some but hopefully libraries that use the API correctly and warn users about incorrect usage emerge.

@anonrig

anonrig commented Feb 10, 2026

Copy link
Copy Markdown
Member Author

@mcollina Thanks for the feedback. I agree the ESM semantics concerns are real. This API doesn’t change the core ESM invariants (single instance per URL); it only removes Node's internal cache entries to allow explicit reloads in opt‑in workflows. Even with that, existing references (namespaces, listeners, closures) can keep old graphs alive, so this is still potentially leaky unless the app does explicit disposal. I’ll make sure the docs call out the risks and the fact that this only clears Node’s internal caches, and I’d like loader team input on the final shape of the API.

This commit should address some of your concerns. b3bd79a

I am still +1 on the feature from a user usability point of view. Code lgtm.

Thanks for the review @benjamingr. Would you mind re-reviewing again so I can trigger CI?

@Nsttt

Nsttt commented Feb 10, 2026

Copy link
Copy Markdown

Thanks a lot for this ❤️

@Jamesernator

Jamesernator commented Feb 10, 2026 •

Copy link
Copy Markdown

Rather than violating ESM invariants, can't node just provide a function that imports a url?

i.e. While the given example of:

const url = new URL('./mod.mjs', import.meta.url);
await import(url.href);

clearCache(url);
await import(url.href); // re-executes the module

is indeed not spec compliant, it's perfectly legal to have something like:

import { clearCache, importModule } from "node:module";

await importModule(someUrl);
clearCache();
await importModule(someUrl); // reexecute

@codecov

codecov Bot commented Feb 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.84536% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.19%. Comparing base (57860ef) to head (d2e22d5).
⚠️ Report is 206 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/modules/clear.js 92.80% 20 Missing and 1 partial ⚠️
src/module_wrap.cc 60.00% 1 Missing and 1 partial ⚠️
src/node_modules.cc 87.50% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61767      +/-   ##
==========================================
+ Coverage   90.14%   90.19%   +0.04%     
==========================================
  Files         769      771       +2     
  Lines      262968   264891    +1923     
  Branches    50052    50320     +268     
==========================================
+ Hits       237049   238912    +1863     
- Misses      16924    16990      +66     
+ Partials     8995     8989       -6     
Files with missing lines Coverage Δ
lib/internal/modules/cjs/loader.js 98.09% <100.00%> (+0.10%) ⬆️
lib/internal/modules/esm/loader.js 99.80% <100.00%> (+<0.01%) ⬆️
lib/internal/modules/esm/module_map.js 98.78% <100.00%> (+0.31%) ⬆️
lib/internal/modules/esm/translators.js 97.69% <100.00%> (+0.12%) ⬆️
lib/internal/modules/helpers.js 98.80% <100.00%> (+0.04%) ⬆️
lib/internal/modules/package_json_reader.js 99.33% <100.00%> (+0.04%) ⬆️
lib/module.js 100.00% <100.00%> (ø)
src/module_wrap.h 52.94% <ø> (ø)
src/node_modules.h 100.00% <ø> (ø)
src/module_wrap.cc 74.09% <60.00%> (-0.12%) ⬇️
... and 2 more

... and 39 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@joyeecheung joyeecheung left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I am +1 to the idea in general, I am afraid the current API may bring more problem than it solves...see the comments.

(Granted it isn't really a problem unique to this specific design, I think the issue is more that this is not a very well solved problem so far, I don't really know what it should look like, though I think I might be able to point out what it should not look like to avoid adding/re-introducing leaks/use-after-frees that user land workarounds can already manage)

Comment thread doc/api/module.md Outdated
Comment thread doc/api/module.md Outdated
Comment thread doc/api/module.md Outdated
Comment thread doc/api/module.md Outdated
Comment thread lib/internal/modules/cjs/loader.js Outdated
Comment thread lib/internal/modules/cjs/loader.js Outdated
@ScriptedAlchemy

ScriptedAlchemy commented Feb 11, 2026 •

Copy link
Copy Markdown

I was the one requesting this while sitting next to yagiz today.
Some context:

We take advantage of Module Federation which allows us to distribute code at runtime. However, when parts of the distributed system are updated, it gets stuck in module cache.

I've had some workarounds, like attempting to purge require cache - however when it comes to esm, it's a difficult problem. Since we do this distribution primarily in production, and there can be thousands of updates a day, I block esm from being supported because it'll leak memory - which was fine for several years but becoming more problematic in modern tooling.

On lambda we cannot just exit a process and bring a new one up without triggering a empty deploy, which has generally been a perf hit to cold start a new lambda vs try and "reset" the module cache for primitive hot reload.

Now, I know this might be controversial, or not recommended - but the reality is that many large companies use federation, most fortune 50 companies use it heavily. All of them are relying on userland cobbling I've created. If there is a solution, it would be greatly appreciated by all of my users.

I believe this would also be very useful in general for tooling like rspack etc where we have universal dev serves.

If invalidation of specific modules causes complexity, I'd be more than happy with a nuclear option like resetModuleCache() which just clears everything entirely. Would be a little slower, but nothing is slower than killing a process and bringing up a new one.

"Soft Restart" node without killing it.
Yes, I'm aware of various footguns like globals, prototype pollution etc.
These so far have been easy to mitigate and none of the user base has reported any major issues around it, whereas my cobbled together solution poses a much bigger issue vs footguns.

Don't have much opinion on spec compliance etc, can go through NAPI as well if that would avoid any spec concerns or pushback.

@jsumners-nr

Copy link
Copy Markdown

Chiming in to say that re-loading a module is very helpful in tests. We can do this with the fabulous CJS paradigm, but ESM does not have a viable equivalent and it should.

@joyeecheung

joyeecheung commented Feb 11, 2026 •

Copy link
Copy Markdown
Member

I think there are still quite a few places that need updates/tests - I tried my best to find them, but there are some dusty corners in the module loader that I have never poked at, you might want to take a heap snapshot or write more tests with v8.queryObject() to verify:

  • What happens when a closure in a module errors (or more specifically when the error stack is prepared by poking at various caches) after the cache of the original module is cleared? Especially if it has source maps and --enable-source-maps is on?
  • This is tricky, but cjsModule[parent] and cjsModule[kLastModuleParent] could need an update too if you yank the parents out of the cache. Otherwise the parent can get leaked.
  • When dynamic import(cjs) happens, there can be a point where the CJS module cache entry for the requested module and its dependencies are synchronously populated for export detection, but they will only be compiled and evaluated in the next microtask queue checkpoint, yet here import() itself can already return since it's async, and some code elsewhere could clear the cache before another checkpoint (likely an await) actually spins the evaluation - in the evaluation callback of cjs facades, it will then try to look up the caches again, and see a mismatch between "module whose exports are detected" v.s. "module that's actually being compiled and evaluated" - races of this kind has been a source of subtle bugs, we sort of made most of them go away by making resolution and loading entirely synchronous, but the cache clearing can expose new internal details that add another bug surface that's worth checking.
  • The cjsCache in the esm translators (there's a TODO about using WeakMap instead, maybe that works?)
  • The wasm facade module has a custom import.meta initializer that contains a closure (implemented in createDynamicModule), which in turn has references crossing the wasm boundary, not sure if that can create another source of leaks.

@anonrig

anonrig commented Feb 11, 2026

Copy link
Copy Markdown
Member Author

I think I addressed all of your concerns @joyeecheung. Let me know if I missed anything!

@GeoffreyBooth

Copy link
Copy Markdown
Member

I’m relatively +1 on having this in Node.js, but I recall having a a lot of discussions about this @GeoffreyBooth and @nodejs/loaders teams about this, and it would massively break the spec, expectations, and invariants regarding ESM.

Just pinging @guybedford to speak on the spec concerns. I think we should wait for him or someone similarly knowledgeable about the spec to comment before landing.

In general I'm +1 on the feature, assuming it can be safely implemented. My (dim) recollection was that the last time we considered it, it was impossible to modify an ES module after it had been loaded into V8. Has that changed in recent years? How do you handle cases like import { foo } from './bar.js' where bar.js gets reloaded and no longer has a foo export, and the importing code calls foo()? That was part of the complexity, that ESM has this linking stage and so presumably replaced modules need to have the same shapes/exports or else the linking gets invalidated.

@anonrig
anonrig requested a review from guybedford February 12, 2026 01:40
@joyeecheung

Copy link
Copy Markdown
Member

Another uncleared cache I happened to notice just now: we hold kLinkedRequestsSlot in ModuleWrap for the modules the module imports, and they are never cleared. I think for the current features we support for ESM, it's fine to clear the array right after instantiation, though.

@Nsttt

Nsttt commented Jul 25, 2026

Copy link
Copy Markdown

@anonrig can you have a look when you have a bit of time. would be lovely to land this

@jasnell jasnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI agent is not permitted to use Signed-off-by.

One of the commits here is missing Signed-off-by completely.

@cursor
cursor Bot force-pushed the yagiz/node-module-clear-cache branch 3 times, most recently from efe37f8 to 8491ce5 Compare August 21, 2026 02:26
@anonrig

anonrig commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

@jasnell fixed

@jasnell
jasnell dismissed their stale review August 21, 2026 02:29

Resolved

@cursor
cursor Bot force-pushed the yagiz/node-module-clear-cache branch from 8491ce5 to 8d8dc58 Compare September 5, 2026 02:51
@anonrig
anonrig requested a review from mcollina September 5, 2026 02:54
Assisted-by: Cursor
Authored-by: Yagiz Nizipli <yagiz@nizipli.com>
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
@cursor
cursor Bot force-pushed the yagiz/node-module-clear-cache branch from 8d8dc58 to d2e22d5 Compare September 5, 2026 13:05
@anonrig
anonrig requested a review from joyeecheung September 5, 2026 13:06

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Sep 5, 2026
@panva panva removed the review wanted PRs that need review. label Sep 7, 2026
@anonrig anonrig added request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. and removed request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. labels Sep 9, 2026
@panva panva removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Sep 24, 2026
@joyeecheung

joyeecheung commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

@Nsttt asked what remains for this PR to land, so leaving my conclusion so far:

  1. There are two types of modules that this PR cover, both CJS and ESM. For CJS this should already be enough to clear all non-user-land references. For ESM it's not, however, because there are a lot more caches that hold ESM alive (CJS is mostly handled by Node.js's JS side and therefore is fairly simple to clear, ESM is managed partially by Node.js native side, partially JS side, and partially by V8, so it's much trickier) . At least two identified caches that are still unhandled are module: add clearCache for CJS and ESM #61767 (comment) (which should be addressed when this lands in V8) and module: add clearCache for CJS and ESM #61767 (comment) (can be handled in this PR) - this means practically, if the user calls module.clearCache on an ESM and expect Node.js/V8 to release references to it - to eliminate the perceived leaks of ESM - they would be disappointed.
  2. If we want to land it as-is I think we need to at least put in a warning in the documentation about 1, that it does not actually clear all the cache for ESM. But then releasing a half-baked API that's supposed to clear the caches for the modules but also is known to fail to clear all the caches for ESM is a bit weird. It would make a bit more sense to at least handle the two known unhandled caches before releasing it.

@jsumners-nr

Copy link
Copy Markdown

Why are there so many places ESM is cached? I would not have been surprised to learn the cache exists solely at the V8 layer, but am very surprised that there are so many caches and that the list may not be comprehensive.

@joyeecheung

Copy link
Copy Markdown
Member

Why are there so many places ESM is cached?

I think the honest answer is that nobody knows. The ESM implementation was just a result of organic growth and things are just scattered everywhere.

@joyeecheung

joyeecheung commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Also another factor is that Node.js CommonJS is not part of the language and completely implemented by Node.js, and primarily implemented in JS land, so it's generally simpler. By design, ESM is partially implemented by the JS engine and partially implemented by the JS engine embedder, the memory management has also been tricky for browsers. And as there are more ESM feature proposals in TC39 being developed, it's expected to become more complex in the future, for example proposals like https://github.com/tc39/proposal-esm-phase-imports may entail changes to the memory management to the embedder-side module map that would have implication for this API too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

esm Issues and PRs related to the ECMAScript Modules implementation. module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. notable-change PRs with changes that should be highlighted in changelogs. semver-minor PRs that contain new features and should be released in the next minor version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.