How to Debug Stub Memory Leaks in Long-Lived Sessions Using CAPNWEB_TRACK_STUBS

Set CAPNWEB_TRACK_STUBS=1 to enable weak-reference tracking of RpcTarget stubs in Cloudflare Computer, then call stubSnapshot() or query GET /__computerd/stubs to detect leaks by verifying all per-class counters reach zero after disposal.

Cloudflare Computer uses Cap'n Proto-Web (capnweb) for the RPC channel between Durable Objects and the computerd daemon, where every cross-boundary object becomes an RpcTarget stub that must be explicitly disposed to prevent memory exhaustion in long-lived isolates. When CAPNWEB_TRACK_STUBS is enabled, the runtime maintains diagnostic counters that track stub lifecycles via trackStub() and untrackStub() without impacting production performance. This guide explains how to activate this subsystem, capture snapshots, and integrate leak detection into your soak tests.

Understanding Stub Memory Leaks in Cap'n Proto-Web

In packages/rpc/src/debug.ts, Cloudflare Computer implements a diagnostic subsystem that monitors RpcTarget instances crossing the RPC boundary. Each stub represents a remote object that should receive [Symbol.dispose] when the session ends or the stub is no longer needed.

In long-lived sessions—such as persistent Workers or containers that continuously fetch new stubs—a missed disposal manifests as a stub memory leak. The stub object persists in the RPC layer even after the remote side discards it, slowly consuming heap space. The tracking mechanism uses a WeakSet to hold live instances and a Map to count objects per class name, ensuring zero production overhead when disabled. As noted in the source, "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…" (debug.ts#L1-L10).

Enabling the CAPNWEB_TRACK_STUBS Diagnostic Mode

You can activate stub diagnostics via environment variable or programmatic API. Set CAPNWEB_TRACK_STUBS=1 before starting the process, or import enableStubTracking() from @cloudflare/computer-rpc/debug:

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

// Explicitly enable if environment variable is not set
enableStubTracking();

When enabled, every RpcTarget-derived class automatically calls trackStub(this) in its constructor. When [Symbol.dispose] executes, untrackStub(this) decrements the corresponding counter. The implementation is deliberately a no-op when tracking is off, ensuring production workloads incur no penalty.

Capturing Stub Snapshots to Detect Leaks

The diagnostic API exposes stubSnapshot(), which returns a plain object mapping class names to live instance counts. A zero value across all keys indicates healthy disposal; non-zero values reveal specific leak locations.

Programmatic inspection:

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

const leaks = stubSnapshot();
// Example output: { DebugTrackerTarget: 2 }
console.log("Live stubs:", leaks);

HTTP endpoint inspection:

When running the computerd daemon with tracking enabled, packages/computerd/src/cli/computerd.ts registers GET /__computerd/stubs to expose the same snapshot. According to the documentation, you can "read stubSnapshot() from @cloudflare/computer-rpc/debug, or hit GET /__computerd/stubs on a computerd instance" (README.md#L391-L393):

export CAPNWEB_TRACK_STUBS=1
curl http://localhost:8787/__computerd/stubs

# => {"DebugTrackerTarget":0}

Soak Testing Workflow for Leak Detection

Integrate stub tracking into long-running tests to catch regressions early. The recommended workflow separates measurement from enforcement, allowing you to assert zero leaks after quiescence:

  1. Enable tracking using CAPNWEB_TRACK_STUBS=1 or enableStubTracking().
  2. Exercise the RPC layer through your workload (e.g., repeated workspace.runtime.exec calls).
  3. Pause execution to allow pending disposals to settle.
  4. Capture a snapshot via stubSnapshot() or the HTTP endpoint.
  5. Assert zero counts for all stub classes; non-zero values indicate the specific RpcTarget implementation with broken disposal contracts.

Complete Debugging Example

The following TypeScript example demonstrates a complete leak detection pattern, including settling time and assertion based on the source test patterns:

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

// Enable diagnostics
enableStubTracking();

// Simulate RPC-heavy workload
for (let i = 0; i < 100; i++) {
  const stub = await workspace.runtime.exec("echo hi");
  // Stub should auto-dispose when RPC completes
}

// Allow disposal callbacks to settle
await new Promise((r) => setTimeout(r, 200));

// Inspect for leaks
const snapshot = stubSnapshot();
if (Object.keys(snapshot).length > 0) {
  throw new Error(`Stub leak detected: ${JSON.stringify(snapshot)}`);
}

For daemon-level monitoring, query the administrative endpoint:

curl -s http://localhost:8787/__computerd/stubs | jq

Key Source Files and Implementation Details

Understanding the source architecture helps interpret leak reports:

Summary

  • Set CAPNWEB_TRACK_STUBS=1 to activate zero-overhead diagnostic tracking of RpcTarget stubs in the Cloudflare Computer runtime.
  • Query stubSnapshot() programmatically or access GET /__computerd/stubs on the daemon to retrieve per-class live counts.
  • Assert zero counters after workload quiescence; non-zero values pinpoint specific stub classes with missing [Symbol.dispose] implementations.
  • Track leaks in soak tests by enabling diagnostics, running sustained RPC workloads, and verifying disposal completeness to prevent long-lived session exhaustion.

Frequently Asked Questions

What causes stub memory leaks in Cloudflare Computer?

Stub leaks occur when RpcTarget objects crossing the Cap'n Proto-Web RPC boundary fail to receive [Symbol.dispose] calls. In long-lived isolates that continuously create new stubs, each missed disposal accumulates orphaned objects in the RPC layer, eventually exhausting available heap memory.

Is CAPNWEB_TRACK_STUBS safe for production environments?

While safe, the tracking adds minimal overhead via WeakSet and Map operations. The subsystem is designed as a no-op when disabled, but the diagnostic mode is intended primarily for development, testing, and debugging sessions rather than high-traffic production deployments.

How do I interpret non-zero values in stubSnapshot()?

Non-zero counts in the snapshot object indicate the exact number of live instances for that specific stub class. For example, { DebugTrackerTarget: 2 } means two instances of that class have not been disposed. Check the corresponding class implementation in your codebase for missing disposal calls or broken disposal contracts.

Can I enable stub tracking without restarting the computerd daemon?

Yes, call enableStubTracking() programmatically from within your Worker or application code before creating RPC stubs. However, to access the HTTP endpoint GET /__computerd/stubs, the daemon must have been started with CAPNWEB_TRACK_STUBS=1 or you must restart it with the variable set.

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 →