Skip to content

Miniflare.dispose() can delay or skip workerd termination behind earlier cleanup awaits #15085

Description

@teamleaderleo

What versions & operating system are you using?

System:
OS: macOS 26.5.1
CPU: (10) arm64 Apple M5
Memory: 286.09 MB / 24.00 GB
Shell: 5.9 - /bin/zsh

Binaries:
Node: 22.23.1 - /Users/leoli/.nvm/versions/node/v22.23.1/bin/node
npm: 10.9.8 - /Users/leoli/.nvm/versions/node/v22.23.1/bin/npm
pnpm: 10.33.0 - /Users/leoli/.nvm/versions/node/v22.23.1/bin/pnpm
bun: 1.3.14 - /Users/leoli/.bun/bin/bun

Reproduced against workers-sdk commit:
99eb50c

Please provide a link to a minimal reproduction

teamleaderleo#55

Describe the Bug

Miniflare.dispose() currently awaits browser cleanup and ProxyClient.dispose() before it calls Runtime.dispose().

As a result, Runtime.dispose() is reached only after those earlier cleanup awaits fulfill.

If either remains pending, runtime disposal is delayed for as long as that cleanup remains pending, leaving the workerd child running. If either rejects, execution can leave the cleanup sequence before Runtime.dispose() is called at all.

Reproduction

I reproduced this with a Miniflare instance and workerd child by holding ProxyClient.dispose() pending for one second.

Current behavior:

[fieldwork-456] before-release {
  "observedForMs": 1002,
  "workerdPid": 85892,
  "killRequested": false,
  "childStillAlive": true,
  "disposeSettled": false
}

After releasing proxy cleanup, execution reaches runtime disposal and the child exits with SIGKILL:

[fieldwork-456] after-release {
  "workerdPid": 85892,
  "exitCode": null,
  "signalCode": "SIGKILL",
  "disposeSettled": true
}

Expected behavior

Independent browser/proxy cleanup shouldn't control whether Miniflare requests termination of the workerd process it owns.

Runtime.dispose() already requests SIGKILL synchronously and returns the child-exit promise, so runtime disposal can be started before awaiting independent cleanup.

The outer Miniflare.dispose() can remain pending until the other cleanup completes.

Direction

I tested starting runtime disposal before the independent cleanup awaits, retaining the returned exit promise, and awaiting it before Miniflare.dispose() settles.

Under the same one-second blocked proxy cleanup:

[fieldwork-456] before-release {
  "observedForMs": 1001,
  "workerdPid": 86121,
  "killRequested": true,
  "childStillAlive": false,
  "disposeSettled": false
}

So workerd has already terminated, while Miniflare correctly continues waiting for the remaining cleanup.

A draft implementation and regression tests following this direction are here:

teamleaderleo#55

There is an additional question of whether every later teardown owner should be attempted after an independent cleanup failure, but the scope of this report is limited to ensuring that an earlier independent cleanup operation won't delay or skip owned runtime termination.

Please provide any relevant error logs

There isn't an error log associated with the pending-cleanup reproduction. The relevant observed process state is:

Current behavior:
[fieldwork-456] before-release {
  "observedForMs": 1002,
  "workerdPid": 85892,
  "killRequested": false,
  "childStillAlive": true,
  "disposeSettled": false
}

After releasing proxy cleanup:
[fieldwork-456] after-release {
  "workerdPid": 85892,
  "exitCode": null,
  "signalCode": "SIGKILL",
  "disposeSettled": true
}

Candidate behavior under the same one-second hold:
[fieldwork-456] before-release {
  "observedForMs": 1001,
  "workerdPid": 86121,
  "killRequested": true,
  "childStillAlive": false,
  "disposeSettled": false
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions