Dev server restarts retain retired environments through options.previousEnvironments
Describe the bug
Repeated server.restart() calls retain retired environment/configuration graphs through _createServer's options.previousEnvironments. The live server keeps its initialization/listen closure, which still references options for options.listen. That options object continues to hold the previous environments after their initialization handoff is complete.
This creates a chain across server generations and can retain old module graphs and plugin caches. In a larger development app this contributed to hundreds of MB of retained heap per full restart and eventual Node heap exhaustion. The reproduction below needs no framework, application code, file watcher, HTTP listener, or browser.
Reproduction
Minimal runnable reproduction, exact package versions, and workarounds.
There is also an independent native callback retention bug in Rolldown #10887. The first workaround step below removes that independent root so the test measures Vite's restart-reference chain.
Steps to reproduce
git clone https://gist.github.com/7db63deba8526a8810164775e9e5f330.git memory-retention-repro
cd memory-retention-repro
npm install
node apply-workaround.mjs rolldown
npm run repro:vite
Expected: the four retired client environments and their configs become collectible while the current public server remains running.
Actual on Vite 8.3.0:
{ checked: 8, retained: 8 }
AssertionError: The running Vite server retains every retired generation
The test holds only WeakRefs to the retired objects and crosses eight event-loop turns with explicit GC before checking. It starts measuring after the first restart because Vite preserves accessors on the original public server object; the assertion concerns subsequent generations. Closing the entire server before checking would hide this growing chain.
Local fix and result
After the awaited environment initialization in _createServer (packages/vite/src/node/server/index.ts), release the initialization-only reference:
options.previousEnvironments = undefined
The previous instance has already been passed to each environment's awaited init at this point. The reproduction includes the equivalent small patch against the published package:
node apply-workaround.mjs vite
npm run repro:vite
Result:
{ checked: 8, retained: 0 }
The before/after test gives the same results on Node 22.22.3, 24.18.0, and 26.7.0. We also applied this cleanup locally to Vite 8.1.4, added an uncached lifecycle regression check, and verified real browser reloads, typecheck, and production builds alongside the separate Rolldown correction. This regression check should inshallah help catch future changes that reintroduce the retention chain.
System Info
OS: Linux x86_64
Node: 22.22.3 / 24.18.0 / 26.7.0
Vite: 8.3.0 (also observed with 8.1.4)
Rolldown: 1.2.8
Framework plugins: none in reproduction
Configuration: configFile=false, middlewareMode=true, watch=null, ws=false
Used Package Manager
npm
Related issues checked
- #22992 addresses an in-flight HMR update racing a restart. This reproduction restarts sequentially and does not dispatch HMR updates.
- #21716 addresses rejected URL-resolution promises. This reproduction resolves an existing package successfully.
- I found no existing issue/PR covering retention through
options.previousEnvironments; the matching reference is still present in the current source.
Source: vitejs/vite