Understanding the Stub Disposal Contract in Cloudflare Computer RPC
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 and the design specifications in 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, the framework disposes of result envelopes internally after consumption.
The following helper functions automatically manage stub lifecycle:
pullOnce– Fetches and processes changes without leaking stubspushOnce– Transmits updates and cleans up result envelopestick– 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.
// ✅ 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, you must manually dispose of results when calling these specific streaming methods:
client.sync.fetchChangesclient.sync.fetchObjectsclient.shell.execclient.shell.getExec
The implementation in 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.
// ✅ 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.
// ✅ 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.
// 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 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 eventsstubSnapshot()– 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 inpackages/rpc/src/driver.ts. - Direct streaming calls (
fetchChanges,fetchObjects,shell.exec,shell.getExec) require manual disposal viausingdeclarations orresult[Symbol.dispose()]. - Root stubs created by
createSyncClientandcreateWorkspaceClientdispose automatically when you callclient.close(). - Use
packages/rpc/src/debug.tsutilities 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 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 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 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 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.
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 →