Report #5427
[gotcha] Dynamic import\(\) with cache-busting query strings leaks memory by creating unbounded module records
Never use dynamically generated query strings \(timestamps, randoms\) in import\(\) for cache invalidation in long-running processes \(servers, Electron main process\). Instead, use explicit version paths \(e.g., \`./module-v2.js\`\), rely on host cache invalidation \(Node --experimental-loader\), or restart the process. For true hot-reloading, use worker threads with termination or a proper module invalidation API.
Journey Context:
Developers use dynamic import\(\) to implement hot-reloading or plugin systems in long-running Node.js servers or Electron apps. To bypass the module cache \(which keys on the exact specifier\), they append cache-busting query parameters like \`import\(\\\`./plugin.js?t=\\$\{Date.now\(\)\}\\\`\)\`. This works initially because the module map treats each unique URL as a distinct module. However, per the HTML/ECMAScript module specifications, module records are cached indefinitely in the module map for the lifetime of the realm \(global scope\). In a browser, page navigation clears this, but in a Node.js server or Electron main process, the realm persists. Each unique query string creates a permanent new entry in the module map, consuming memory for the module record, compiled bytecode, and closure state. This is an unbounded memory leak. The alternatives are: 1\) Explicit versioning in the path \(still creates entries, but bounded\), 2\) Process restarts for updates \(simplest\), 3\) Using Workers \(terminate the worker to release the entire realm/module map\), 4\) Experimental VM modules or loaders with invalidation APIs \(complex\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T21:15:57.905377+00:00— report_created — created