Cloudflare Durable Object and computerd Sync Protocol Architecture
The sync protocol relies on a single-direction, text-based Cap’n Proto RPC transmitted over a long-lived WebSocket connection, where both the Durable Object and computerd container implement a symmetrical WorkspaceRPC interface split into SyncRPC for filesystem replication and ShellRPC for command execution.
The architecture enables real-time synchronization of a SQLite-backed virtual filesystem between Cloudflare’s edge-hosted Durable Objects and stateful computerd containers. As implemented in the cloudflare/computer repository, this protocol ensures consistency across distributed environments through a cursor-based streaming model and content-addressed blob deduplication.
WorkspaceRPC Interface Design
The protocol centers on a unified WorkspaceRPC interface defined in [packages/rpc/src/interface.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts). This contract is intentionally symmetrical—both the Durable Object (client) and the computerd container (server) implement identical interfaces—though the transport initiates from the DO side.
The interface bifurcates into two distinct halves:
| Half | Purpose | Key Methods |
|---|---|---|
| SyncRPC | Replicates the SQLite-backed virtual filesystem | push, pushObjects, fetchChanges, fetchObjects, hasObjects, readEntry, watermarks |
| ShellRPC | Executes and manages shell commands inside the container | exec, getExec, killExec, disposeExec |
This design allows both filesystem synchronization and remote command execution to traverse the same Cap’n Proto stub while remaining independently testable.
Transport Layer and Connection Management
WebSocket Transport with HTTP Fallback
The primary transport is a single persistent WebSocket connection opened by the Durable Object to the computerd container:
- Endpoint:
ws://<container>:45678/ws - Fallback: HTTP-batch endpoint at
/apifor environments that cannot maintain persistent sockets - Framing: Cap’n Proto text frames (binary frame support is not yet implemented)
The WebSocket carries all RPC traffic for both SyncRPC and ShellRPC operations multiplexed over the same socket.
Reconnection Strategy
If the WebSocket closes or encounters an error, the Durable Object side tears down the stub synchronously. The next RPC call automatically establishes a new connection to the still-running computerd process. This stateless reconnection model ensures that transient network failures do not require manual intervention, though in-flight operations must be retried by the caller.
Cursor-Based Synchronization Model
The sync protocol uses a cursor model to enable exactly-once delivery and resume interrupted streams. The cursor structure is defined in [packages/dofs/src/sync/changes.ts](https://github.com/cloudflare/computer/blob/main/packages/dofs/src/sync/changes.ts):
type ChangeCursor = {
rev: number;
path: string | null
};
A path: null cursor indicates that the entire revision has been offered, while a specific path indicates a partial revision transmission that can resume mid-stream.
Pull Cycle Implementation
The bidirectional driver in [packages/rpc/src/sync-driver.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) implements the pullOnce function to fetch remote changes:
- Calls
fetchChanges(after)with the local cursor to streamChangeEntrybatches (bounded byPULL_BATCH_SIZE = 256) - Probes for existing blobs using
hasObjectsto avoid redundant transfers - Streams missing blobs via
fetchObjectsasReadableStream<{hash, bytes}> - Applies the
ChangeEntrybatch locally usingapplyChanges - Advances the local fetch cursor to
currentCursor
The driver detects watermark divergence (indicating a computerd restart or data loss) and automatically resets the local cursor for a single retry when first detected.
Push Cycle Implementation
The pushOnce function mirrors the pull logic in the opposite direction:
- Streams local changes via
pushas aReadableStream<ChangeEntry> - The remote probes for missing blobs with
hasObjects - Transfers missing blobs via
pushObjects - Returns the new revision and
appliedPushCursorconfirming what the remote applied
Blob Deduplication and Content Addressing
All file system objects are content-addressed by hash. Before transferring bytes, the sender calls hasObjects to query which hashes the recipient already possesses. Only missing blobs traverse the wire, ensuring efficient synchronization even when files share common chunks or undergo minor modifications.
Error Handling and Observability
Structured Error Codes
Errors propagate as structured WireError objects (lines 55-66 of interface.ts) and re-throw as WorkspaceError on the client. Critical codes include:
ENOENT: Missing path or execution IDEUNKNOWN_HASH: Request for a blob not present in the remote storeELOG_TRUNCATED: Execution log has been evicted from memory
RPC Telemetry
The createSyncClient function in [packages/rpc/src/client.ts](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) accepts an optional onRPCEvent callback that receives { rpc, durationMs, ok, code? } for each RPC invocation, enabling integration with distributed tracing systems and performance monitoring.
Implementation Examples
Creating a Sync Client
import { createSyncClient } from "@cloudflare/computer/rpc";
const rpc = createSyncClient({
url: "ws://127.0.0.1:45678/ws",
onRPCEvent: (ev) => console.log(ev), // Optional observability hook
});
// Push local changes to the container
await rpc.sync.push({
senderRev: 0, // 0 indicates external orchestrator
changes: myChangesStream, // ReadableStream<ChangeEntry>
});
Pulling Remote Changes
import { pullOnce } from "@cloudflare/computer/rpc";
import { openDatabase } from "@cloudflare/dofs";
const db = await openDatabase(/* ... */);
const remote = createSyncClient({ url: "ws://<do-host>/ws" });
const result = await pullOnce(db, remote.sync);
console.log(`Applied ${result.applied} entries, new cursor: ${result.cursor}`);
Executing Shell Commands
import { createWorkspaceClient } from "@cloudflare/computer/rpc";
const ws = createWorkspaceClient({ url: "ws://127.0.0.1:45678/ws" });
const { id, events } = await ws.shell.exec({
source: "ls -la /",
});
for await (const ev of events) {
if (ev.name === "stdout") process.stdout.write(ev.value);
if (ev.name === "stderr") process.stderr.write(ev.value);
}
await ws.shell.disposeExec({ id });
Summary
- The WorkspaceRPC interface unifies filesystem sync and shell execution under a single Cap’n Proto contract implemented symmetrically by both Durable Objects and computerd.
- Transport occurs over persistent WebSockets with text-based Cap’n Proto framing and automatic reconnection logic.
- Synchronization uses a cursor-based model (
ChangeCursor) supporting resume-capable streams and bounded batch processing (256 entries per pull). - Content addressing via
hasObjectseliminates redundant blob transfers across the network. - Error handling uses structured codes (
ENOENT,EUNKNOWN_HASH) propagated throughWireErrorobjects. - Observability hooks in the client constructor enable per-RPC metrics collection without modifying business logic.
Frequently Asked Questions
How does the sync protocol handle computerd container restarts?
The driver detects watermark divergence when the remote’s reported revision regresses or jumps unexpectedly. Upon first detection, it resets the local fetch cursor to zero and performs a single retry, effectively resynchronizing the filesystem from scratch without data loss.
What is the purpose of the hasObjects method in the sync protocol?
hasObjects enables content-addressed deduplication by allowing the sender to query which blob hashes the recipient already possesses before transferring bytes. This prevents redundant network traffic when files share common chunks or undergo minor modifications.
Can the sync protocol operate without WebSocket support?
Yes. While the primary transport uses WebSocket (ws://<container>:45678/ws), an HTTP-batch endpoint at /api provides a fallback for environments that cannot maintain persistent socket connections, though with higher latency due to connection overhead per request.
How are concurrent filesystem modifications handled during synchronization?
The protocol relies on revision numbers and cursor-based streaming rather than locking. Each push or fetchChanges call operates on a consistent snapshot at a specific revision. Conflicts are resolved at the application layer by the SQLite-backed virtual filesystem, not by the RPC protocol itself.
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 →