Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[vitest-plugin] introspectWorkflowInstance never disposes its InstanceModifier stub, so workerd warns "An RPC stub was not disposed properly"

Open Beginner friendly
#16,255 2 comments 0 reactions 0 assignees View on GitHub

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

bug package:vitest
What versions & operating system are you using?
  • @cloudflare/vitest-plugin 1.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

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.

More from cloudflare/workers-sdk

All issues in cloudflare/workers-sdk

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.