# Stub Disposal Contract in Cloudflare Computer: Preventing Memory Leaks in Long-Lived Sessions

> Understand the stub disposal contract in Cloudflare Computer. Learn how to prevent memory leaks in long-lived sessions by manually disposing remote stub objects with Symbol.dispose.

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

---

**The stub disposal contract is an explicit agreement between Cap'n Web (capnweb) RPC peers that requires manual disposal of remote stub objects via `Symbol.dispose` to prevent unbounded memory growth.**

Capnweb does not garbage collect remote stubs automatically. A stub referenced on one side keeps the peer's resources alive until the WebSocket closes or the owning side explicitly releases it. In long-lived sessions—such as agent loops or frequently invoked Workers handlers—neglecting this contract causes memory leaks that eventually trigger OOM failures.

## Understanding the Two Stub Boundaries

The Cloudflare Computer repository defines two distinct boundaries where stubs cross, each with specific disposal rules.

### Worker/DO ↔ computerd (Capnweb over WebSocket)

The sync driver in [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) manages RPC calls like `fetchChanges`, `pushObjects`, and `exec` streams. Callers must bind result envelopes to `using` variables or call `result[Symbol.dispose]()` after draining streams. The driver wraps disposal in `finally` blocks using `maybeDispose(fetchResult)` to guarantee cleanup on errors.

### Worker ↔ DO (Workers-RPC)

Workers obtain a `WorkspaceStub` via `env.COMPUTERD.get(id).getWorkspace()`. This parent stub includes child objects (`fs`, `runtime`, `runtime.exec` handles). Disposing the parent cascades to children on the DO side. Individual child stubs cannot be disposed independently—they live only as long as the parent workspace stub.

## How to Prevent Memory Leaks

### Use `using` Declarations for Automatic Cleanup

The `using` keyword ensures `Symbol.dispose` executes when the block exits. This is the recommended pattern for all stub acquisitions.

```typescript
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const id = env.COMPUTERD.idFromName("user-123");
    using ws = await env.COMPUTERD.get(id).getWorkspace();   // ← disposes ws
    using execHandle = await ws.runtime.exec("npm test");   // ← disposes handle
    const result = await execHandle.result();                // plain data
    return Response.json({ exitCode: result.exitCode });
  },
} satisfies ExportedHandler<Env>;

```

This pattern appears in [`docs/11_lifecycle.md`](https://github.com/cloudflare/computer/blob/main/docs/11_lifecycle.md) (lines 202–227) as the canonical implementation.

### Manual Disposal for Streaming Results

When `using` is unavailable—such as direct `client.sync.fetchChanges` calls—dispose manually in a `finally` block.

```typescript
const { stream } = await client.sync.fetchChanges({ after });

try {
  for await (const entry of stream) {
    // process entry …
  }
} finally {
  (stream as any)[Symbol.dispose]?.();
}

```

The `maybeDispose` helper in [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) (lines 39–45) implements this fallback pattern.

### Enable Stub Tracking in Development

Set `CAPNWEB_TRACK_STUBS=1` to expose live stub counts. Import `stubSnapshot` from `@cloudflare/computer-rpc/debug` or query the `computerd` endpoint.

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

if (process.env.CAPNWEB_TRACK_STUBS) {
  console.log("Live stubs:", stubSnapshot());
}

```

The `computerd` CLI exposes this at `GET /__computerd/stubs` (see [`packages/computerd/src/cli/computerd.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/cli/computerd.ts), lines 236–247).

### Validate with the Stub Soak Test

[`packages/computer/tests/stub-soak.test.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/tests/stub-soak.test.ts) (lines 31–71) creates and destroys stubs repeatedly while asserting that `stubSnapshot()` returns to baseline. Run this test to verify your disposal patterns under sustained load.

```typescript
// Simplified soak test structure from the source
import { stubSnapshot } from "@cloudflare/computer-rpc/debug";

test("no stub leaks after repeated workspace acquisitions", async () => {
  const baseline = stubSnapshot();
  
  for (let i = 0; i < 1000; i++) {
    using ws = await getWorkspace();  // proper disposal
    await ws.runtime.exec("echo test");
  }
  
  expect(stubSnapshot()).toBe(baseline);
});

```

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`docs/11_lifecycle.md`](https://github.com/cloudflare/computer/blob/main/docs/11_lifecycle.md) | Documents the stub disposal contract and its two boundaries |
| [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) | Implements `maybeDispose`, drives sync ticks, guarantees envelope cleanup |
| [`packages/computerd/src/cli/computerd.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/cli/computerd.ts) | Exposes `/__computerd/stubs` debugging endpoint |
| [`packages/computer/tests/stub-soak.test.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/tests/stub-soak.test.ts) | Validates leak-free operation under load |
| [`packages/computer/src/shell.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/shell.ts) | Example driver with `using` bindings for streaming results |
| [`packages/computer/src/debug.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/debug.ts) | Provides `stubSnapshot()` for leak detection |

## Summary

- **The stub disposal contract requires explicit cleanup**—capnweb has no automatic garbage collection for remote stubs.
- **Two boundaries exist**: Worker/DO↔computerd (capnweb) and Worker↔DO (Workers-RPC), with cascading disposal on the parent stub.
- **Prefer `using` declarations** for automatic disposal of workspace stubs and exec handles.
- **Use `finally` blocks with manual `Symbol.dispose` calls** when `using` is unavailable.
- **Enable `CAPNWEB_TRACK_STUBS`** and run soak tests to detect leaks during development.

Following these patterns ensures that every remote stub is released promptly, keeping long-lived sessions stable and memory-bounded.

## Frequently Asked Questions

### What happens if I forget to dispose a stub?

The remote resource remains allocated on the peer until the WebSocket connection closes. In long-lived sessions, repeated unclosed stubs accumulate, causing memory growth that eventually exhausts available heap and triggers OOM failures.

### Can I dispose child stubs like `ws.runtime.exec` independently?

No. Child stubs such as `fs`, `runtime`, and `runtime.exec` handles are not independently disposable. Only the parent `WorkspaceStub` supports disposal, which cascades to all its children on the DO side according to the implementation in [`docs/11_lifecycle.md`](https://github.com/cloudflare/computer/blob/main/docs/11_lifecycle.md).

### Does `using` work with streaming responses?

Yes. The `using` declaration works with any object implementing `Symbol.dispose`, including streaming result envelopes from `fetchChanges` or `exec` calls. The stream itself must be fully drained or explicitly closed before the `using` block exits.

### How do I verify my code has no stub leaks?

Enable `CAPNWEB_TRACK_STUBS=1`, import `stubSnapshot` from `@cloudflare/computer-rpc/debug`, and assert that stub counts return to baseline after operations. For comprehensive validation, run the official soak test in [`packages/computer/tests/stub-soak.test.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/tests/stub-soak.test.ts).