How to Track Stub Leaks in Cloudflare Computer Using `CAPNWEB_TRACK_STUBS` and `stubSnapshot()`
Set the CAPNWEB_TRACK_STUBS environment variable to 1 or call enableStubTracking() to activate per-class stub counting, then invoke stubSnapshot() to retrieve a live map of reference counts that exposes leaks in your Cap'n Web RPC layer.
Cloudflare Computer uses Cap'n Web RPC to expose objects across isolate boundaries, creating lightweight proxies called stubs that must be explicitly disposed to prevent memory leaks. The cloudflare/computer repository includes a zero-overhead diagnostic system—controlled by the CAPNWEB_TRACK_STUBS flag—that instruments every RpcTarget constructor and reports real-time statistics via the stubSnapshot() utility.
Understanding Stub Lifecycle and Leak Detection
In the RPC layer, every remotely accessible object extends RpcTarget. When an instance is created, the base class invokes trackStub(this) defined in packages/rpc/src/debug.ts. This function increments a counter stored in a Map<string, number> keyed by the constructor name, while a companion WeakSet guards against double-counting the same object. When the stub is disposed via [Symbol.dispose], the proxy invokes untrackStub(), which decrements the counter and deletes the map entry when the count reaches zero. If a stub is never disposed, its counter remains positive, signaling a leak.
Enabling Stub Tracking
The instrumentation is disabled by default to eliminate runtime overhead. You can activate it via an environment variable or programmatically.
Environment Variable Configuration
At module load, packages/rpc/src/debug.ts evaluates process.env.CAPNWEB_TRACK_STUBS (Node.js) or globalThis.CAPNWEB_TRACK_STUBS (Workerd):
// packages/rpc/src/debug.ts
const trackingEnabled =
process.env.CAPNWEB_TRACK_STUBS === "1" ||
globalThis.CAPNWEB_TRACK_STUBS === true;
When this condition is truthy, isStubTrackingEnabled() returns true and the counting machinery activates.
Programmatic Activation
For test harnesses where environment variables do not propagate, import enableStubTracking() and call it before running assertions:
import { enableStubTracking } from "@cloudflare/computer-rpc/debug";
enableStubTracking(); // Forces tracking regardless of env var state
Capturing Snapshots with stubSnapshot()
Once tracking is active, call stubSnapshot() to capture the current state of live stubs. The function returns a plain object mapping class names to active instance counts:
import { stubSnapshot } from "@cloudflare/computer-rpc/debug";
const counts = stubSnapshot();
console.log(counts); // { MyService: 3, AnotherTarget: 0 }
If isStubTrackingEnabled() returns false, stubSnapshot() returns an empty object {} to ensure production code does not crash.
Monitoring Leaks via HTTP Endpoint
The computerd CLI exposes these metrics via an HTTP endpoint. In packages/computerd/src/cli/computerd.ts, the request handler imports stubSnapshot and returns its JSON serialization at /debug/stubs:
// packages/computerd/src/cli/computerd.ts (approx. line 243)
body = request.method === "HEAD"
? ""
: JSON.stringify(stubSnapshot());
You can inspect stub counts without modifying application code:
curl http://localhost:8787/debug/stubs
Practical Implementation in Tests
The repository provides a reference implementation in packages/computer/tests/stub-soak.test.ts. The test enables tracking, exercises RPC boundaries, and asserts that stubSnapshot() reports zero lingering stubs:
import { enableStubTracking, stubSnapshot } from "@cloudflare/computer-rpc/debug";
// Enable tracking before RPC operations
enableStubTracking();
// ... run workloads that create and dispose stubs ...
const snap = stubSnapshot();
const allClean = Object.values(snap).every(v => v === 0);
expect(allClean).toBe(true);
The companion file packages/computer/tests/stub-soak-worker.ts forwards the snapshot call across the isolate boundary, allowing the test harness to verify leak-free behavior in both the host and the worker.
Summary
- Activation: Set
CAPNWEB_TRACK_STUBS=1or callenableStubTracking()frompackages/rpc/src/debug.tsto turn on counting. - Mechanism:
RpcTargetconstructors invoketrackStub(), incrementing a per-class counter guarded by aWeakSet; disposal triggersuntrackStub()to decrement it. - Inspection: Use
stubSnapshot()to retrieve live counts as a plain object, or query the/debug/stubsendpoint served bycomputerdinpackages/computerd/src/cli/computerd.ts. - Validation: Assert that
stubSnapshot()returns an empty object or zero values after test runs to confirm all stubs were properly disposed, following the pattern inpackages/computer/tests/stub-soak.test.ts.
Frequently Asked Questions
What triggers a stub leak in Cloudflare Computer?
A stub leak occurs when an RpcTarget instance is created but never disposed via its [Symbol.dispose] method. Because stubs maintain references to remote objects across isolate boundaries, failing to dispose them prevents garbage collection and wastes memory. The trackStub() function in packages/rpc/src/debug.ts increments a counter on construction, and stubSnapshot() reveals any counts that remain positive after the expected lifetime of the object.
How do I enable stub tracking in production environments?
You should avoid enabling stub tracking in production because it consumes additional memory for the Map and WeakSet structures and CPU cycles for counting. If you must diagnose a leak in a live system, restart the isolate with CAPNWEB_TRACK_STUBS=1, capture snapshots via the /debug/stubs endpoint defined in packages/computerd/src/cli/computerd.ts, and then remove the flag once debugging is complete.
What does stubSnapshot() return when tracking is disabled?
When isStubTrackingEnabled() returns false (the default state), stubSnapshot() immediately returns an empty object {}. This behavior ensures that production code checking for leaks does not throw or produce undefined results when the diagnostic flag is absent, as implemented in packages/rpc/src/debug.ts.
Where is trackStub called in the RPC lifecycle?
trackStub() is invoked inside the RpcTarget base class constructor, which all RPC-exposed objects extend. This guarantees every stub is accounted for at the moment of creation. The corresponding untrackStub() call is triggered when the stub proxy's [Symbol.dispose] method is invoked, typically via explicit disposal or scoped usage with using declarations.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →