[vitest-plugin] introspectWorkflowInstance never disposes its InstanceModifier stub, so workerd warns "An RPC stub was not disposed properly"
Maintainers usually reply within 1 day
@akhil9tiet is already working on this.
Since Oct 14, 2026.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 70/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- testing
Research direction
The leak is in WorkflowInstanceIntrospectorHandle in packages/workflows-shared/src/introspection.ts. Check the constructor's #instanceModifier and #instanceModifierPromise fields and what dispose() and Symbol.asyncDispose currently do. Reproduce with the inline test from the issue, running it several times to see the warning. Done means dispose() also releases the modifier stub and the warning no longer appears when the test never calls modify().
Written by the indexing model from the issue text.
Description
What versions & operating system are you using?
@cloudflare/vitest-plugin1.4.0- Miniflare 5.20261006.1-alpha, workerd 1.20261006.1
- Wrangler 4.149.0, Vitest 5.0.3
- Node 22.22.3, macOS 27.0.1 (arm64)
Please provide a link to a minimal reproduction
Inline below (a single test file; any Workflow binding works, and the instance never has to run).
Describe the Bug
introspectWorkflowInstance() returns a WorkflowInstanceIntrospectorHandle. Its constructor eagerly calls workflow.unsafeGetInstanceModifier(instanceId) and keeps the resulting InstanceModifier RPC stub in #instanceModifier. dispose() / [Symbol.asyncDispose]() only calls unsafeAbort(instanceId, ...) and never disposes that stub. When the stub is garbage-collected, workerd prints:
An RPC stub was not disposed properly. You must call dispose() on all stubs in order to let the other side know that you are no longer using them. ...
This happens on every test that uses introspectWorkflowInstance, even one that never calls modify(), because the stub is fetched in the constructor. Since it depends on GC timing, the warning comes and goes between runs, which makes it look like a flaky test.
Expected: disposing the introspector (await using or dispose()) releases every stub it acquired, so no warning is printed.
Actual: the warning is printed. Disposing the stub yourself inside modify() makes it go away, which shows the leaked stub is the modifier:
import { env, introspectWorkflowInstance } from "cloudflare:test";
import { it } from "vitest";
const MODE: "none" | "modify" | "modify-dispose" = "none";
it("introspected instances release their stubs", async () => {
for (let i = 0; i < 20; i++) {
await using instance = await introspectWorkflowInstance(env.MY_WORKFLOW, `probe-${i}`);
if (MODE !== "none") {
await instance.modify(async (m) => {
await m.disableRetryDelays();
if (MODE === "modify-dispose") (m as unknown as Disposable)[Symbol.dispose]();
});
}
}
// Give GC a chance to collect the stubs before the run ends.
for (let i = 0; i < 50; i++) new Array(1e6).fill(i);
await new Promise((r) => setTimeout(r, 500));
});
Three runs of each mode:
MODE |
"RPC stub was not disposed" printed |
|---|---|
"none" (introspect only, no modify()) |
3 / 3 |
"modify" |
3 / 3 |
"modify-dispose" (test disposes m itself) |
0 / 3 |
Suggested fix: in WorkflowInstanceIntrospectorHandle (packages/workflows-shared/src/introspection.ts, bundled into dist/worker/lib/cloudflare/test-internal.mjs), have dispose() call this.#instanceModifier?.[Symbol.dispose](), or dispose it once #instanceModifierPromise settles if it's still pending. Fetching the modifier lazily in modify() instead of the constructor would also avoid acquiring it for tests that never modify.
Related but separate: #15832 (workflow-wide introspection acquires a modifier for every instance).
Please provide any relevant error logs
An RPC stub was not disposed properly. You must call dispose() on all stubs in order to let the other side know that you are no longer using them. You cannot rely on the garbage collector for this because it may take arbitrarily long before actually collecting unreachable objects. As a shortcut, calling dispose() on the result of an RPC call disposes all stubs within it.
- Dominant language
- TypeScript
- Stars
- 4.6k
- Forks
- 1.6k
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 258
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from cloudflare/workers-sdk
-
miniflare D1: a comment at offset 0 in a batch element errors locally but succeeds remotelyPossibly taken @akhil9tiet claimed this today. Openbug package:miniflare product:d1
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
cloudflare/workers-sdk#16319 · 2 comments ·
Maintainers usually reply within 1 day
-
feature:auto-config package:c3
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
cloudflare/workers-sdk#16294 · 2 comments ·
Maintainers usually reply within 1 day
-
`wrangler d1 migrations list --remote` ignores `account_id` from the config filePossibly taken @idhanth claimed this 1 day ago. Openbug package:wrangler product:d1
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
cloudflare/workers-sdk#16292 · 2 comments ·
Maintainers usually reply within 1 day
-
bug package:vitest
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
cloudflare/workers-sdk#16238 · 1 comment ·
Maintainers usually reply within 1 day
-
wrangler dev: a CORS preflight that meets a dropped internal connection is not retriedPossibly taken @akhil9tiet claimed this 5 days ago. Openbug package:wrangler start-dev-worker
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
cloudflare/workers-sdk#16216 · 5 comments ·
Maintainers usually reply within 1 day
All issues in cloudflare/workers-sdk
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 73/100
anomalyco/opencode#56380 · 1 comment ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
BUG | `MCP 服务器` | 全局层行的停用/启用与删除报 404 server row not found,编辑保存会在 profile 层复制出重复行Possibly taken @w6242652 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Chocobozzz/PeerTube#7832 ·
Maintainers usually reply within 3 days
-
Pay to use?Open
Difficulty 1/5 Under an hour Newbie friendliness 66/100
darkreader/darkreader#16108 · 1 comment ·
Maintainers usually reply within 2 days