Stub Leaks in Long-Lived Sessions: How to Debug with CAPNWEB_TRACK_STUBS

Stub leaks occur when remote RPC targets (stubs) aren't properly disposed after use, causing memory growth in long-lived sessions like Durable Objects; the CAPNWEB_TRACK_STUBS environment flag enables lightweight per-class counters to pinpoint exactly which stub types remain alive.

In the Cloudflare Computer project, the Cap'n Proto-based RPC layer (CapnWeb) creates a stub for every remote object returned by an RPC call. These stubs must be explicitly disposed when no longer needed. When they're not, you get a stub leak—a silent memory growth problem that primarily surfaces in long-lived sessions such as Durable Objects that stay resident for minutes or hours.

What Causes Stub Leaks in Long-Lived Sessions

Four primary root causes lead to stub leaks according to the cloudflare/computer source code. Understanding each helps you systematically eliminate leaks in production code.

Forgotten Disposal of Result Envelopes

When a client receives an RPC result containing stubs, the caller—not the framework—must invoke [Symbol.dispose]() on the envelope. Omitting this call leaves the underlying stub counted as live in the Durable Object's internal tracker.

Broken Disposal Contract

CapnWeb may invoke [Symbol.dispose] multiple times for shared targets, particularly when sessions close. The tracking code guards against double-counting, but never invoking dispose at all means the counter never decrements. This is the most subtle failure mode: the code appears to work, yet memory grows unbounded.

Leakage Across Composite Stubs

Composite stubs like stub.sync or stub.push forward operations to nested stubs. If any nested stub escapes disposal, the parent's live count persists indefinitely. These chains require careful audit of every stub reference in composite operations.

Missing Tracking Enablement

The stub-tracking instrumentation is disabled by default in production builds to ensure zero runtime overhead. Tests or debugging sessions that forget to enable tracking will miss leaks entirely—giving false confidence.

How CAPNWEB_TRACK_STUBS Enables Debugging

The repository implements a lightweight stub-leak tracker in packages/rpc/src/debug.ts. When activated via CAPNWEB_TRACK_STUBS, every RpcTarget constructor calls trackStub(this), maintaining per-class live counters in a WeakSet.

The tracker exposes three public helpers from @cloudflare/computer-rpc/debug:

  • enableStubTracking() — programmatically forces tracking on, useful in workerd where environment variables may not propagate
  • stubSnapshot() — returns Record<string, number> with current live counts per stub class
  • isStubTrackingEnabled() — confirms whether tracking is active

The implementation comment in debug.ts (lines 1–11) clarifies the design philosophy:

// Stub leak tracking. Gated on the CAPNWEB_TRACK_STUBS env flag so
// production paths pay nothing. Every RpcTarget we own opts in by
// calling `trackStub(this)` in its constructor; capnweb may invoke
// `[Symbol.dispose]` more than once for a shared target as sessions
// end, so repeated disposals for the same object are ignored.
//
// The point is *measurement*, not enforcement: snapshot() returns the
// per‑class live count so a soak script can assert "after a quiet
// point, every counter is zero." Anything non‑zero is a leak —
// either we forgot to dispose a result envelope on the caller side,
// or the disposal contract broke.

When the flag is unset, stubSnapshot() returns an empty object—literally zero overhead.

Three Ways to Enable CAPNWEB_TRACK_STUBS

1. Environment Variable

Set CAPNWEB_TRACK_STUBS=1 before launching computerd. The CLI server exposes a diagnostic endpoint at /__computerd/stubs returning JSON snapshot data. Implementation in packages/computerd/src/cli/computerd.ts (lines 236–243):

CAPNWEB_TRACK_STUBS=1 ./computerd
curl http://localhost:8080/__computerd/stubs

2. Global Override

Assign globalThis.CAPNWEB_TRACK_STUBS = "1" in test harnesses. The readFlag() function in debug.ts checks this global when the environment variable isn't available:

// In test setup
(globalThis as any).CAPNWEB_TRACK_STUBS = "1";

3. Programmatic Call

Invoke enableStubTracking() directly:

import { enableStubTracking } from "@cloudflare/computer-rpc/debug";

enableStubTracking();

Practical Debugging Workflow

A typical leak detection pattern compares snapshots before and after workload execution:

import { stubSnapshot, isStubTrackingEnabled } from "@cloudflare/computer-rpc/debug";

if (isStubTrackingEnabled()) {
  console.log("Initial stub counts:", stubSnapshot());
}

// Execute your long-lived workload
await runExtendedOperations();

// Allow GC to settle, then inspect
const after = stubSnapshot();
console.log("Final stub counts:", after);

if (Object.keys(after).length) {
  throw new Error(`Stub leak detected: ${JSON.stringify(after)}`);
}

The stub-soak test suite at packages/computer/tests/stub-soak.test.ts implements exactly this pattern. Running with CAPNWEB_TRACK_STUBS=1 (configured in wrangler.stub-soak.jsonc), it asserts counters return to baseline between operations. Any non-zero value fails the test, exposing the specific stub class and leak source.

Key Source Files for Reference

File Purpose
packages/rpc/src/debug.ts Core tracking: trackStub, untrackStub, stubSnapshot, flag handling
packages/computerd/src/cli/computerd.ts HTTP endpoint /__computerd/stubs exposing snapshot JSON
packages/computer/tests/stub-soak.test.ts Long-lived session test asserting zero stub leaks
script/computerd-stub-soak.mjs Example soak harness with tracking enabled
docs/11_lifecycle.md Durable Object lifecycle documentation, leak detection guidance

Summary

  • Stub leaks stem from forgotten envelope disposal, broken disposal contracts, nested stub leakage, or disabled tracking
  • CAPNWEB_TRACK_STUBS gates zero-overhead instrumentation that counts live stubs per class via WeakSet
  • Three activation methods: environment variable, global override, or enableStubTracking() call
  • stubSnapshot() provides concrete, testable evidence of which stub types leak
  • The stub-soak test suite demonstrates systematic leak prevention in CI

Frequently Asked Questions

What exactly is a stub in the Computer project?

A stub is a local proxy object representing a remote RPC target in the CapnWeb RPC layer. Each time an RPC call returns an object, CapnWeb creates a stub on the caller side that must be explicitly disposed via [Symbol.dispose]() when no longer needed.

Why do stub leaks primarily affect long-lived sessions?

Short-lived processes terminate before leak accumulation becomes visible. Durable Objects and similar long-running contexts stay resident for minutes or hours, allowing unstopped stub creation to manifest as measurable memory growth and eventual performance degradation.

Can I use CAPNWEB_TRACK_STUBS in production?

Yes, but the intention is debugging and testing. Production builds disable tracking by default to guarantee zero overhead. Enabling it in production provides diagnostic data at the cost of minor runtime overhead and increased memory for the WeakSet and counters.

How do I interpret a non-zero stubSnapshot result?

A non-zero count for a specific stub class indicates that instances of that class were created but never disposed. Use the class name to locate the corresponding RPC call sites in your code, then verify that every result envelope and composite stub receives proper [Symbol.dispose]() invocation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →