sveltejs / sveltejs/kit

`emulate()` never disposes the platform proxy, so every build leaks a `workerd`

Open
#16,919 4 comments 1 reaction 0 assignees View on GitHub

A pull request for this has already been merged.

  • #16754 by @ottomated — merged
pkg:adapter-cloudflare
Dominant language
JavaScript
Stars
20.8k
Forks
2.3k
Avg merge
1d 16h
Merged PRs (30d)
156

Description

Describe the bug
Describe the bug

@sveltejs/adapter-cloudflare's emulate() calls wrangler's getPlatformProxy(), which
starts a miniflare workerd child process. The adapter never calls proxy.dispose() — the
string dispose does not appear anywhere in the package.

The adapter returns only { platform } from emulate(), and the proxy is captured in a closure, so nothing downstream can dispose it either — a userland wrapper cannot fix this without reimplementing emulate().

Why the leaked ports are not harmless

A wildcard bind does not reserve a port. The kernel's wildcard ephemeral allocator will hand out 0.0.0.0:N while the leaked workerd still holds 127.0.0.1:N; both binds coexist, and every connection goes to the most specific matching listener. So an unrelated program that binds 0.0.0.0:N and then dials 127.0.0.1:N reaches the leaked workerd instead of itself and blocks.

We ran into this but running scala tests: sbt.ForkTests binds its fork handshake with new ServerSocket(0) (wildcard) and its child dials 127.0.0.1 so on a machine where a SvelteKit app had been built, an unrelated Scala repo's test runner sat in accept() until the leaked workerd's idle timeout.

Why it is invisible

src/utils/fork.js carries the comment:

Runs a task in a subprocess so any dangling stuff gets killed upon completion.

It does not run a subprocess. It runs a node:worker_threads Worker, which shares the process so a child process the task spawned is not killed when the worker finishes, and is not killed when the worker is unref'd and torn down at process exit either. It is inherited by init.

Suggested fix
  1. Emulator gains an optional teardown. emulate() returns { platform } and the type has no disposal hook, so an adapter that acquires a resource has no way to release it.
    Adding dispose?: () => MaybePromise<void> to Emulator and awaiting it at the three call sites src/core/postbuild/prerender.js after prerendering, and the dev and preview servers on close would make this expressible.
  2. adapter-cloudflare implements it, holding the getPlatformProxy() result and calling proxy.dispose().
Reproduction

Any SvelteKit app on @sveltejs/adapter-cloudflare with export const prerender = true:

npm ci
lsof -c workerd -a -d cwd -Fn | grep -c "$PWD"    # 0
npm run build
lsof -c workerd -a -d cwd -Fn | grep -c "$PWD"    # 1

The leaked process:

$ ps -o pid=,ppid=,command= -p <pid>
25193  1  .../node_modules/@cloudflare/workerd-darwin-arm64/bin/workerd serve --binary ...
$ lsof -nP -p <pid> -a -iTCP -sTCP:LISTEN
workerd 25193 ... TCP 127.0.0.1:60893 (LISTEN)
workerd 25193 ... TCP 127.0.0.1:60894 (LISTEN)

Bisected against the other steps of the same CI script on a clean clone — vitest run, svelte-kit sync, svelte-check, playwright test --list all leave the count unchanged; vite build is the step that adds one.

Logs

System Info
- `@sveltejs/adapter-cloudflare` 7.2.4 and 7.2.9; also present in `8.0.0-next.6`, which still contains no `dispose`
- `@sveltejs/kit` 2.x
- `vite` 8
- macOS (arm64); the mechanism is not platform-specific
Severity

annoyance

Additional Information

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/utils/fork.js and src/core/postbuild/prerender.js, then trace the dev and preview server close paths and adapter-cloudflare's emulate() entry point. Reproduce with a prerendered build and inspect workerd processes before and after. Done means the platform proxy is disposed across build, dev, and preview lifecycles without leaving workerd processes behind.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js, vite
Domain
build-system, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.