Summary
In a Nuxt SSR app running on Vue 3.5.13, we see very high process RSS spikes under sustained load.
- Without custom per-request scope cleanup, RSS peaks at ~2.5-2.8 GB.
- When we explicitly track and stop all effect scopes created during a request, peak drops to ~436 MB.
This suggests potential issues around request-bound reactivity/effect scope lifecycle cleanup under SSR.
Environment
- Vue:
3.5.13
- Runtime context: SSR through Nuxt/Nitro (
nuxt@3.17.5, nitro@2.11.12)
- Node:
22.x
- Platform tested: macOS (Apple Silicon)
Load profile
npx autocannon -d 60 -c 30 http://127.0.0.1:3100/
- RSS sampled every second
A/B observed data
| Variant |
BEFORE_KB |
MAX_KB |
AFTER_KB |
IDLE20_KB |
| default (no extra cleanup) |
82704 |
2825840 |
1916608 |
48880 |
| custom scope cleanup |
38576 |
435936 |
368784 |
110016 |
Additional default run:
MAX_KB=2770272, AFTER_KB=1366000, IDLE20_KB=50416
What our custom cleanup does
As an experiment, we patched build output to:
- Track all effect scopes created during SSR request execution using AsyncLocalStorage.
- After request render, stop tracked scopes in reverse order.
With this approach, peak memory dropped significantly.
Why we suspect Vue reactivity/effect lifecycle
The strongest signal is that explicit post-request scope.stop() for all request-created scopes changes memory profile dramatically.
Package toggles in surrounding stack did not materially remove the problem:
@pinia/nuxt@0.5.5: MAX_KB=2669616
@nuxtjs/i18n@10.0.6: MAX_KB=2731184
experimental.asyncContext=true: MAX_KB=2513312
Expected behavior
Request-scoped reactive effects should be fully released/disposed after SSR render completion so that sustained traffic does not produce multi-GB peak RSS in this scenario.
Actual behavior
Under sustained SSR traffic, process RSS reaches ~2.5-2.8 GB unless we forcibly stop tracked request scopes.
Questions for Vue maintainers
- Are there known SSR edge cases where request-created
effectScope instances can outlive request lifecycle unintentionally?
- Is there a recommended, supported way to ensure complete disposal of request-scoped effects in SSR integrations?
- Are there diagnostics/hooks in Vue reactivity internals we can use to confirm orphaned effects in production-like workloads?
If helpful, we can provide logs and a minimized reproduction (likely via Nuxt SSR harness) focusing only on the reactivity lifecycle aspect.
Summary
In a Nuxt SSR app running on Vue
3.5.13, we see very high process RSS spikes under sustained load.This suggests potential issues around request-bound reactivity/effect scope lifecycle cleanup under SSR.
Environment
3.5.13nuxt@3.17.5,nitro@2.11.12)22.xLoad profile
npx autocannon -d 60 -c 30 http://127.0.0.1:3100/A/B observed data
Additional default run:
MAX_KB=2770272,AFTER_KB=1366000,IDLE20_KB=50416What our custom cleanup does
As an experiment, we patched build output to:
With this approach, peak memory dropped significantly.
Why we suspect Vue reactivity/effect lifecycle
The strongest signal is that explicit post-request
scope.stop()for all request-created scopes changes memory profile dramatically.Package toggles in surrounding stack did not materially remove the problem:
@pinia/nuxt@0.5.5:MAX_KB=2669616@nuxtjs/i18n@10.0.6:MAX_KB=2731184experimental.asyncContext=true:MAX_KB=2513312Expected behavior
Request-scoped reactive effects should be fully released/disposed after SSR render completion so that sustained traffic does not produce multi-GB peak RSS in this scenario.
Actual behavior
Under sustained SSR traffic, process RSS reaches ~2.5-2.8 GB unless we forcibly stop tracked request scopes.
Questions for Vue maintainers
effectScopeinstances can outlive request lifecycle unintentionally?If helpful, we can provide logs and a minimized reproduction (likely via Nuxt SSR harness) focusing only on the reactivity lifecycle aspect.