How the Capnweb RPC Protocol Connects Durable Objects to computerd
The capnweb RPC protocol operates over a single long-lived WebSocket on /api, using text-frame JSON to expose a composite WorkspaceRPC interface that splits into SyncRPC for filesystem operations and ShellRPC for command execution.
The capnweb RPC protocol serves as the exclusive communication channel between Cloudflare Durable Objects (DO) and the sandboxed computerd process in the cloudflare/computer repository. This lightweight, text-based protocol enables real-time filesystem synchronization and remote shell execution across a single WebSocket transport, with automatic reconnection handling on the client side.
WebSocket Transport and Session Lifecycle
The protocol relies on a single HTTP GET request to /api that upgrades into a long-lived WebSocket connection. In packages/rpc/src/client.ts, the Durable Object creates the RPC stub before the WebSocket handshake completes, queuing any calls until the socket is ready. Once established, the connection persists for the entire workspace lifetime.
Binary frames are explicitly unsupported; all messages use text-frame JSON format. If the socket closes or errors, the DO synchronously discards the stub. Subsequent RPC calls automatically trigger a fresh connection attempt, ensuring transparent re-establishment of the session without client intervention.
The WorkspaceRPC Interface Architecture
At the core of the protocol is the WorkspaceRPC interface defined in packages/rpc/src/interface.ts. This root stub merges two independent APIs under one surface:
export interface WorkspaceRPC {
sync: SyncRPC;
shell: ShellRPC;
}
The DO accesses these halves via rpc.sync and rpc.shell, allowing a single connection to handle both filesystem state and process execution. The server implementation in packages/rpc/src/server.ts constructs this composite stub and wires it to the WebSocket endpoint.
Filesystem Synchronization via SyncRPC
The SyncRPC interface handles all virtual filesystem operations between the DO and computerd.
Pushing Changes with Revision Tracking
The push method streams a batch of ChangeEntry records containing paths and hashes. A critical parameter is senderRev:
senderRev > 0: Indicates the push originates from a sync peersenderRev === 0: Indicates the push comes from an external orchestrator
The container replies with its new revision and a cursor marking the applied batch, allowing precise synchronization tracking.
Streaming Remote Changes
The fetchChanges method accepts a cursor—structured as { rev: number, path: string | null }—and returns a ReadableStream<ChangeEntry>. When path is null, the stream includes all changes up to the specified revision. This enables efficient incremental sync without polling.
Object Management and Diagnostics
Additional methods provide visibility into container state:
watermarks: Returns current revision markershasObjects,fetchObjects,pushObjects: Efficient bulk transfer of file contentsreadEntry: Direct metadata inspection
These primitives allow the DO to maintain consistency while minimizing data transfer.
Remote Execution via ShellRPC
The ShellRPC interface provides sandboxed command execution inside the computerd container.
Starting Processes
The exec method accepts a command or module source and returns an execution handle. The handle includes an events field—a ReadableStream<ExecEvent>—that delivers stdout, stderr, and exit frames in real time.
Process Lifecycle Management
The DO manages running processes through three key methods:
getExec: Re-attaches to an existing execution by IDkillExec: Signals a running process to terminatedisposeExec: Cleans up resources and closes the execution context
This design supports long-running tasks that may outlive individual WebSocket connections, with the ability to reconnect and resume monitoring.
Client Implementation and Reconnection
The createWorkspaceClient helper in packages/rpc/src/client.ts manages connection state. It buffers RPC calls made before the WebSocket opens, flushing them immediately upon connection. When the socket closes, the client discards the underlying stub; the next method invocation automatically creates a fresh session, ensuring resilience against network interruptions.
Protocol Versioning Constraints
The capnweb protocol currently has no built-in version negotiation. Any change to request or response shapes constitutes a hard breaking change, requiring lock-step deployment of both DO and computerd components. This design trades flexibility for simplicity, assuming both endpoints run compatible code versions.
Practical Implementation Example
The following pattern from packages/computer/src/backends/container/cloudflare-container.ts demonstrates typical usage:
import { createWorkspaceClient } from "@cloudflare/computer-rpc";
// Create client stub (queues calls until WebSocket ready)
const rpc = await createWorkspaceClient("ws://localhost:45678/api");
// Push filesystem changes as a sync peer
await rpc.sync.push({
senderRev: 42,
changes: new ReadableStream({
start(controller) {
controller.enqueue({
path: "/src/main.ts",
hash: new Uint8Array([/* ... */])
});
controller.close();
},
}),
});
// Fetch remote changes since revision 42
const { stream } = await rpc.sync.fetchChanges({
after: { rev: 42, path: null },
});
// Execute shell command and stream output
const { id, events } = await rpc.shell.exec({
source: "ls -l /workspace",
});
for await (const ev of events) {
if (ev.name === "stdout") {
console.log(new TextDecoder().decode(ev.value));
}
if (ev.name === "exit") {
console.log(`Process exited with code ${ev.code}`);
}
}
Summary
- Transport: Single WebSocket on
/apiusing text-frame JSON only; binary frames are unsupported. - Interface:
WorkspaceRPCcombinesSyncRPCandShellRPCunder one root stub defined inpackages/rpc/src/interface.ts. - Sync: Push changes with
senderRevsemantics, fetch incremental updates via cursors, and transfer file objects efficiently. - Execution: Spawn processes with
exec, managing lifecycle throughgetExec,killExec, anddisposeExecwith streaming event output. - Resilience: Client-side stub recreation and call buffering ensure transparent reconnection without application-level handling.
- Versioning: Hard protocol compatibility requires synchronized deployment; no runtime negotiation exists.
Frequently Asked Questions
What transport mechanism does the capnweb RPC protocol use?
The protocol uses a single long-lived WebSocket connection established via an HTTP GET upgrade to /api. All communication occurs over text frames containing JSON payloads; binary WebSocket frames are explicitly unsupported by the current implementation.
How does the Durable Object handle connection failures?
The DO immediately discards the RPC stub when the WebSocket closes or errors. The client implementation in packages/rpc/src/client.ts automatically creates a fresh stub on the next method call, buffering any queued requests until the new connection establishes, ensuring seamless reconnection without manual intervention.
What is the difference between SyncRPC and ShellRPC?
SyncRPC manages filesystem state through methods like push, fetchChanges, and watermarks, handling revision-tracked file synchronization. ShellRPC provides process execution capabilities via exec, killExec, and related methods, returning streaming output and exit codes from commands running inside the computerd container.
Why does the protocol lack version negotiation?
The capnweb RPC protocol omits versioning to minimize complexity and overhead. Because the DO and computerd are typically deployed together as a unified system, the implementation assumes both endpoints run compatible code versions. Any schema changes require coordinated, lock-step rollouts to prevent deserialization errors.
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 →