Skip to content

fix(server): prevent RequestEvent retention in AsyncLocalStorage async resources after SSR#15770

Open
Zelys-DFKH wants to merge 4 commits into
sveltejs:mainfrom
Zelys-DFKH:fix/ssr-als-memory-leak
Open

fix(server): prevent RequestEvent retention in AsyncLocalStorage async resources after SSR#15770
Zelys-DFKH wants to merge 4 commits into
sveltejs:mainfrom
Zelys-DFKH:fix/ssr-als-memory-leak

Conversation

@Zelys-DFKH
Copy link
Copy Markdown

@Zelys-DFKH Zelys-DFKH commented Apr 28, 2026

closes #15764

Hey! I tracked down what's causing the heap to grow under SSR load with Svelte 4 apps.

What's going on

with_request_store wraps the SSR render in als.run(store, renderFn). Node.js has a quirk in async context propagation (nodejs/node#53408): every async resource created inside renderFn (Promise continuations, callbacks, the works) gets kResourceStore = store stamped onto it internally. In Svelte 4 apps, component_subscribe creates subscription callbacks inside this context that can outlive the render. Those callbacks hold a strong reference to store, so the RequestStore and its RequestEvent never get garbage-collected. Under any real load, this accumulates fast.

Svelte 5 isn't affected because subscriptions are released per-component immediately. Svelte 4 batches them until the full component tree renders.

The fix

Instead of passing store directly to als.run(), I wrap it in a single-property container ({ current: store }) and pass that instead. Async resources created during rendering hold a reference to the lightweight container, not the store itself.

with_request_store gets a gc_barrier parameter (default false). Only the SSR render call in render.js passes gc_barrier = true; after the render promise settles, container.current is nulled. That severs the only path from lingering Svelte 4 subscription callbacks back to the RequestStore, and normal GC takes over.

Other callers (command handlers, query batches, form handlers) keep gc_barrier = false. Their async continuations (the await 0 in get_response, the setTimeout(0) in batch processing) need store access for the duration of the request, so nulling would break them. Scoping the barrier to the render path leaves those flows untouched.

// in with_request_store:
const container = { current: store };
const result = als.run(container, fn);
// gc_barrier = true (SSR render only): container.current = null after render settles

// try_get_request_store dereferences through .current:
return sync_store ?? als?.getStore()?.current ?? null;

Changes

  • packages/kit/src/exports/internal/event.js — wrap ALS value in a container; add gc_barrier parameter (default false) to null the container only when the caller opts in
  • packages/kit/src/runtime/server/page/render.js — pass gc_barrier = true to the with_request_store call that wraps the component render (the only source of this leak)
  • packages/kit/src/exports/internal/event.spec.js — new unit tests including a regression test that spawns a long-lived async resource inside als.run() with gc_barrier = true and asserts it cannot reach the store after render completes (fails on pre-fix code)

All existing tests pass. Happy to adjust anything — thanks for all the work you put into this.


Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

Zelys-DFKH and others added 2 commits April 28, 2026 14:24
…er SSR

Wrap the store passed to `als.run()` in a single-property container so
that async resources created inside the render callback (Promise
continuations, Svelte 4 component_subscribe callbacks, etc.) only retain
a reference to the container rather than the full RequestStore. After the
render promise settles we null out `container.current`, allowing the
RequestStore and RequestEvent to be garbage-collected even when stale
async resources from the render still linger in memory.

Without this fix, every `als.run(store, render)` call causes Node.js to
associate `kResourceStore = store` with every async resource created
during the render (nodejs/node#53408). Under load, hundreds of
RequestEvent objects accumulate as those resources are not GC'd, leading
to linear heap growth and eventual OOM.

Fixes sveltejs#15764

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: a1192cb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot
Copy link
Copy Markdown

Zelys-DFKH and others added 2 commits April 28, 2026 14:38
… in event.spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Narrows the AsyncLocalStorage container-nulling fix to the SSR render
call in render.js, which is the only context where async resources
(Svelte 4 component_subscribe callbacks) can outlive the request and
cause a memory leak.

Other with_request_store callers (command handlers, query batch, etc.)
now receive gc_barrier=false (the default), so their async continuations
(get_response await-0 deferral, batch setTimeout callbacks) can still
read the store for the duration of the request.

Fixes the query.batch refresh-in-command single-flight regression by
restoring store access for those async resources without reintroducing
the original memory leak.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak in SSR with Svelte 4 since with_event was added to render.js in 2.27.0

1 participant