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 /api for 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:

  1. Calls fetchChanges(after) with the local cursor to stream ChangeEntry batches (bounded by PULL_BATCH_SIZE = 256)
  2. Probes for existing blobs using hasObjects to avoid redundant transfers
  3. Streams missing blobs via fetchObjects as ReadableStream<{hash, bytes}>
  4. Applies the ChangeEntry batch locally using applyChanges
  5. 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:

  1. Streams local changes via push as a ReadableStream<ChangeEntry>
  2. The remote probes for missing blobs with hasObjects
  3. Transfers missing blobs via pushObjects
  4. Returns the new revision and appliedPushCursor confirming 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 ID
  • EUNKNOWN_HASH: Request for a blob not present in the remote store
  • ELOG_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 hasObjects eliminates redundant blob transfers across the network.
  • Error handling uses structured codes (ENOENT, EUNKNOWN_HASH) propagated through WireError objects.
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →