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

> Debug stub leaks in long-lived sessions with CAPNWEB_TRACK_STUBS. Learn how this tool tracks and identifies un-disposed stub types causing memory growth in Durable Objects.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: debugging
- Published: 2026-08-14

---

**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`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/debug.ts) (lines 1–11) clarifies the design philosophy:

```ts
// 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`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/cli/computerd.ts)** (lines 236–243):

```bash
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`](https://github.com/cloudflare/computer/blob/main/debug.ts) checks this global when the environment variable isn't available:

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

```

### 3. Programmatic Call

Invoke `enableStubTracking()` directly:

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

enableStubTracking();

```

## Practical Debugging Workflow

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

```ts
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`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/debug.ts) | Core tracking: `trackStub`, `untrackStub`, `stubSnapshot`, flag handling |
| [`packages/computerd/src/cli/computerd.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/cli/computerd.ts) | HTTP endpoint `/__computerd/stubs` exposing snapshot JSON |
| [`packages/computer/tests/stub-soak.test.ts`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/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.