# Understanding the Stub Disposal Contract in Cloudflare Computer RPC

> Learn about the stub disposal contract in Cloudflare Computer RPC. Prevent memory leaks by understanding automatic cleanup and manual disposal of streaming stub results.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: deep-dive
- Published: 2026-08-13

---

**The stub disposal contract in Cloudflare Computer RPC requires developers to either rely on high-level driver helpers for automatic cleanup or manually dispose of streaming stub results using `using` declarations or explicit `Symbol.dispose()` calls to prevent memory leaks in capn web WebSocket sessions.**

The `cloudflare/computer` repository implements a distributed computing platform where **stubs** serve as client-side proxy objects representing remote RPC targets over capn web connections. Managing the lifecycle of these stubs correctly is essential to avoid exhausting the WebSocket's export table and causing memory leaks in long-running sessions. This article explains the disposal contract as implemented in the `@cloudflare/computer-rpc` package, referencing the source code in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) and the design specifications in [`docs/11_lifecycle.md`](https://github.com/cloudflare/computer/blob/main/docs/11_lifecycle.md).

## What Is the Stub Disposal Contract?

In the Cloudflare Computer RPC system, stubs maintain references to remote capabilities within a capn web session. When you invoke streaming methods that return result envelopes, the RPC framework creates temporary stub objects that persist in memory until explicitly released. The **stub disposal contract** defines the obligations for cleaning up these objects, distinguishing between automatic disposal through high-level abstractions and manual disposal required when interacting directly with low-level client methods.

## Automatic Disposal via Driver Helpers

The high-level driver utilities handle stub cleanup automatically, shielding callers from manual memory management. When you use the driver helpers exported from [`packages/rpc/src/driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/driver.ts), the framework disposes of result envelopes internally after consumption.

The following helper functions automatically manage stub lifecycle:

- **`pullOnce`** – Fetches and processes changes without leaking stubs
- **`pushOnce`** – Transmits updates and cleans up result envelopes
- **`tick`** – Performs periodic synchronization with automatic disposal

Because these helpers wrap the underlying RPC calls, you do not need to invoke disposal methods when using them.

```typescript
// ✅ Automatic disposal via high-level driver
import { pullOnce } from "@cloudflare/computer-rpc/driver";

await pullOnce(client);   // Internally disposes result envelopes

```

## Manual Disposal Requirements

When you bypass the driver layer and invoke streaming methods directly through `client.sync` or `client.shell`, you **inherit the disposal contract**. According to the documentation in [`packages/rpc/README.md`](https://github.com/cloudflare/computer/blob/main/packages/rpc/README.md), you must manually dispose of results when calling these specific streaming methods:

- `client.sync.fetchChanges`
- `client.sync.fetchObjects`
- `client.shell.exec`
- `client.shell.getExec`

The implementation in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) attaches a `[Symbol.dispose]` method to result objects. You must invoke this method after draining the stream to release the stub from capn web's export table.

### Using Explicit Resource Management

TypeScript 5.2 and later support the `using` keyword for explicit resource management through the `Symbol.dispose` protocol. Binding the result to a `using` variable ensures automatic disposal when the block scope exits.

```typescript
// ✅ Automatic disposal via using declaration (TS 5.2+)
using result = await client.sync.fetchChanges({ rev: 42 });

for await (const change of result) {
  console.log(change);
}
// Stub automatically disposed when block exits

```

### Explicit Symbol.dispose() Calls

For environments without `using` support or when you need finer control over disposal timing, manually invoke the dispose method after consuming the stream.

```typescript
// ✅ Manual disposal with explicit Symbol.dispose()
const result = await client.shell.exec({ cmd: ["ls", "-la"] });

for await (const chunk of result) {
  console.log(chunk);
}

result[Symbol.dispose]();   // Required: releases the stub

```

## Root Stub Lifecycle Management

The root stubs created by `createSyncClient` and `createWorkspaceClient` follow a different disposal pattern. These constructors arrange for automatic cleanup when you terminate the connection, so you do not need to dispose of the root stub manually.

Calling `client.close()` triggers the disposal sequence before the underlying WebSocket tears down, ensuring all root capabilities release cleanly.

```typescript
// Root stub disposed automatically on close
const client = createSyncClient(socket);
// ... perform operations ...
await client.close();   // Cleans up root stub

```

## Debugging Stub Leaks

The [`packages/rpc/src/debug.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/debug.ts) module provides utilities for tracking stub accumulation during development. You can enable tracking to monitor export table growth and identify code paths that fail to dispose of stubs.

- **`enableStubTracking()`** – Activates instrumentation to log stub creation and disposal events
- **`stubSnapshot()`** – Captures the current state of active stubs for comparison

Use these tools to verify that your implementation adheres to the contract, particularly when working with streaming responses that may not immediately manifest as memory issues.

## Summary

- **Stubs** are client-side proxies for remote RPC targets that require explicit cleanup to prevent memory leaks in capn web sessions.
- **High-level drivers** (`pullOnce`, `pushOnce`, `tick`) automatically dispose of stub envelopes in [`packages/rpc/src/driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/driver.ts).
- **Direct streaming calls** (`fetchChanges`, `fetchObjects`, `shell.exec`, `shell.getExec`) require manual disposal via `using` declarations or `result[Symbol.dispose()]`.
- **Root stubs** created by `createSyncClient` and `createWorkspaceClient` dispose automatically when you call `client.close()`.
- Use [`packages/rpc/src/debug.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/debug.ts) utilities to track and diagnose stub leaks during development.

## Frequently Asked Questions

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

If you fail to dispose of a stub after consuming a streaming result, the object remains in capn web's export table indefinitely. This leaks memory on both the client and server sides and can eventually exhaust the WebSocket's capacity for new exports, causing the session to fail. The design spec in [`docs/11_lifecycle.md`](https://github.com/cloudflare/computer/blob/main/docs/11_lifecycle.md) emphasizes that lingering stubs are the primary source of resource exhaustion in long-lived RPC sessions.

### Can I use the `using` keyword with all RPC methods in Cloudflare Computer?

You can use the `using` keyword with any method that returns a disposable stub object, but it is specifically recommended for the streaming methods in `client.sync` and `client.shell`. The high-level driver helpers in [`packages/rpc/src/driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/driver.ts) handle disposal internally, so wrapping those calls with `using` is unnecessary and provides no additional benefit.

### How do I know if a method requires manual disposal?

Check whether you are calling the method directly on `client.sync` or `client.shell` versus using the driver helpers. Methods documented in [`packages/rpc/README.md`](https://github.com/cloudflare/computer/blob/main/packages/rpc/README.md) under the "Stub disposal" section—specifically `fetchChanges`, `fetchObjects`, `shell.exec`, and `shell.getExec`—require manual disposal. If the method returns a stream or result envelope and you are not using `pullOnce`, `pushOnce`, or `tick`, you must implement the disposal contract.

### Is there a performance penalty for calling Symbol.dispose() multiple times?

The `Symbol.dispose` implementation in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) is idempotent and safe to call multiple times. While the contract requires at least one call after stream consumption, redundant invocations do not throw errors or cause undefined behavior. However, best practice suggests calling it exactly once when the resource is no longer needed.