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
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.
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
Describe the bug
Describe the bug
@sveltejs/adapter-cloudflare'semulate()calls wrangler'sgetPlatformProxy(), whichstarts a miniflare
workerdchild process. The adapter never callsproxy.dispose()— thestring
disposedoes not appear anywhere in the package.The adapter returns only
{ platform }fromemulate(), and the proxy is captured in a closure, so nothing downstream can dispose it either — a userland wrapper cannot fix this without reimplementingemulate().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:Nwhile the leakedworkerdstill holds127.0.0.1:N; both binds coexist, and every connection goes to the most specific matching listener. So an unrelated program that binds0.0.0.0:Nand then dials127.0.0.1:Nreaches the leakedworkerdinstead of itself and blocks.We ran into this but running scala tests:
sbt.ForkTestsbinds its fork handshake withnew ServerSocket(0)(wildcard) and its child dials127.0.0.1so on a machine where a SvelteKit app had been built, an unrelated Scala repo's test runner sat inaccept()until the leakedworkerd's idle timeout.Why it is invisible
src/utils/fork.jscarries the comment:It does not run a subprocess. It runs a
node:worker_threadsWorker, 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
Emulatorgains 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>toEmulatorand awaiting it at the three call sitessrc/core/postbuild/prerender.jsafter prerendering, and the dev and preview servers on close would make this expressible.adapter-cloudflareimplements it, holding thegetPlatformProxy()result and callingproxy.dispose().Reproduction
Any SvelteKit app on
@sveltejs/adapter-cloudflarewithexport const prerender = true:The leaked process:
Bisected against the other steps of the same CI script on a clean clone —
vitest run,svelte-kit sync,svelte-check,playwright test --listall leave the count unchanged;vite buildis the step that adds one.Logs
System Info
Severity
annoyance
Additional Information
No response