Workspace Lifecycle and Stub Disposal Contract in Cloudflare Computer
Cloudflare Computer binds a Durable Object to a containerized computerd process where RPC stubs crossing Worker-DO or Worker-computerd boundaries must be explicitly disposed via Symbol.dispose or using declarations to prevent resource leaks, while the Capnweb session terminates when its WebSocket closes.
The cloudflare/computer repository implements a distributed computing runtime that synchronizes a Cloudflare Durable Object (DO) with a container running the computerd process. Understanding the workspace lifecycle and stub disposal contract is critical for application stability, as the system spans three distinct persistence layers—each with different survival guarantees—and requires manual cleanup of RPC stubs to avoid memory exhaustion. Unlike traditional garbage collection, Capnweb stubs pin remote resources until explicitly released or until the underlying connection drops.
The Three Layers of the Workspace Lifecycle
The architecture separates state across three components, each with unique persistence characteristics documented in [docs/11_lifecycle.md](https://github.com/cloudflare/computer/blob/main/docs/11_lifecycle.md).
Durable Object Persistence
The DO serves as the authoritative state machine for the workspace. According to the source documentation, the DO survives restarts and evictions but loses all in-memory state.
- Survives:
ctx.storage(SQLite database including VFS watermark rows) - Lost: In-memory runtime handles such as the
Workspaceinstance’s#handle,#shell, and#readyPromise
On every incarnation, the DO must reconstruct these handles by reconnecting to the container.
Container Volatility
The container runs the computerd process and exposes a FUSE mount at MOUNT_POINT. It operates independently of the DO and may restart without warning.
- Survives: The in-memory Virtual File System (VFS) only while the process remains alive
- Lost: The FUSE mount,
computerdprocess state, and all Capnweb session data when the container restarts
The container dials back to the DO via WebSocket, making the DO the server in this relationship to support future hibernation features.
Capnweb Session Transience
Capnweb serves as the RPC layer between the DO and container, but it is bound to the transport connection.
- Survives: Only while the WebSocket remains open
- Lost: Export/answer tables, pending RPC promises, active streams, and the socket itself upon disconnection
When the socket closes, the session is discarded. A fresh POST /connect handshake initiated by Workspace.ready() establishes a new session.
The Stub Disposal Contract
Between lines 202-246 of [packages/computer/src/stub.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/stub.ts), the codebase defines a strict disposal contract. Capnweb does not automatically garbage-collect remote stubs; a stub referenced on one side pins resources on the remote side until the WebSocket closes or until the owning side invokes Symbol.dispose.
Boundary One: Worker to Computerd
Cross-boundary calls occur when Workers interact with the container process directly. Streaming RPC methods such as pullOnce, pushOnce, and workspace.runtime.exec return result envelopes that hold resources.
The driver code in [packages/rpc/src/sync-driver.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) binds these envelopes to using variables, which automatically dispose the stub when the scope exits. If you invoke these methods directly without the driver helpers, you must either bind the result with using or manually call result[Symbol.dispose]() after draining the stream.
Boundary Two: Worker to Durable Object
Workers obtain top-level stubs via env.COMPUTERD.get(id).getWorkspace(), which returns a WorkspaceStub. Two first-level stubs require explicit disposal:
WorkspaceStub– The root workspace handleWorkspaceRuntimeExecHandleStub– Returned byws.runtime.exec()
Child stubs such as WorkspaceFilesystemStub and WorkspaceRuntimeStub are not independently disposable. Their lifetimes are bound to the parent stub; disposing the parent automatically cascades disposal to all children.
Correct Usage Patterns
Always use the using keyword (Explicit Resource Management) or manual disposal for first-level stubs. The following pattern demonstrates proper cleanup:
export default {
async fetch(request: Request, env: Env) {
const id = env.COMPUTERD.idFromName("user-123");
using ws = await env.COMPUTERD.get(id).getWorkspace(); // Disposes WorkspaceStub
using handle = await ws.runtime.exec("npm test"); // Disposes exec handle
const result = await handle.result(); // Read result
return Response.json({ exitCode: result.exitCode });
// Both stubs auto-dispose here
},
} satisfies ExportedHandler<Env>;
If a stub is retained across await points, binding it with using prevents leaks. Short-lived requests may tolerate occasional undisposed stubs, but long-running agents or frequent exec calls accumulate resources until the WebSocket closes.
Monitoring and Validation
The repository provides tools to verify stub disposal under load. The soak tests in [packages/computer/tests/stub-soak.test.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/tests/stub-soak.test.ts) and the standalone script script/computerd-stub-soak.mjs expose GET /__computerd/stubs for runtime inspection, allowing developers to detect leaks before they exhaust memory.
Summary
- The workspace lifecycle spans three layers: the durable Durable Object, the volatile container, and the transient Capnweb session.
- The Durable Object persists SQLite state via
ctx.storagebut loses all in-memory handles on restart, requiring reconnection viaWorkspace.ready()and thePOST /connecthandshake. - Capnweb stubs do not auto-release; they must be disposed via
Symbol.disposeor theusingkeyword when crossing Worker boundaries. - First-level stubs (
WorkspaceStub,WorkspaceRuntimeExecHandleStub) require explicit disposal, while child stubs are cleaned up automatically when their parent is disposed. - Long-running applications must rigorously dispose stubs to prevent memory leaks that persist for the duration of the WebSocket connection.
Frequently Asked Questions
What happens if I forget to dispose a stub?
Undisposed stubs continue to pin resources on the remote side until the underlying WebSocket closes. In short-lived requests, this may go unnoticed, but long-running agents or high-frequency exec calls will accumulate leaked stubs, eventually exhausting memory or hitting resource limits.
Do child stubs need individual disposal?
No. Child stubs such as WorkspaceFilesystemStub and WorkspaceRuntimeStub are bound to their parent stub. Disposing the parent automatically cascades disposal to all children, so you only need to manage the lifecycle of first-level stubs.
How long does a Capnweb session last?
A Capnweb session lasts exactly as long as its underlying WebSocket connection remains open. When the socket closes—whether due to network interruption, container restart, or Durable Object eviction—the session is discarded along with all its export tables and pending promises. A new session is established on the next Workspace.ready() call.
What survives a Durable Object eviction?
Only ctx.storage survives, which includes the SQLite database and VFS watermark rows. All in-memory state—including the Workspace instance, its #handle, #shell, and any JavaScript objects—is lost and must be reconstructed when the DO wakes on the next request.
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 →