# Cloudflare Durable Object and computerd Sync Protocol Architecture

> Discover the Cloudflare Durable Object and computerd sync protocol architecture. Learn about its single-direction Cap Proto RPC over WebSockets for filesystem replication and command execution.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: architecture
- Published: 2026-08-07

---

**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)](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)](https://github.com/cloudflare/computer/blob/main/packages/dofs/src/sync/changes.ts):

```typescript
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)](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`](https://github.com/cloudflare/computer/blob/main/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)](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

```typescript
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

```typescript
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

```typescript
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.