# How the Sync Protocol Works Between Cloudflare Durable Objects and computerd Containers

> Discover how the Cloudflare Durable Objects sync protocol ensures data consistency with computerd containers. Learn about bidirectional replication and authoritative sources.

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

---

**The sync protocol uses a bidirectional replication layer built on the `SyncRPC` interface, where the container pulls changes via `fetchChanges` and pushes via `push`, while the Durable Object serves as the authoritative source, reconciling watermarks to maintain consistency.**

The sync protocol enables eventual consistency between Cloudflare Durable Objects and computerd containers by implementing a bidirectional RPC contract. This protocol powers the replication layer in the `cloudflare/computer` repository, ensuring that SQLite database states remain synchronized across edge and containerized environments.

## Sync Protocol Architecture and Data Flow

The protocol operates through two primary directions: the Durable Object (DO) acts as the server exposing `push` and `pushObjects` methods, while the container acts as the client calling `fetchChanges` and `fetchObjects`. Both sides implement the `SyncRPC` interface defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts), creating a symmetric contract where data flows in opposite directions depending on which side initiates the sync.

- **DO → Container**: The Durable Object streams change entries and missing blob bytes via `push` and `pushObjects`.
- **Container → DO**: The container pulls changes via `fetchChanges` and retrieves missing blobs via `fetchObjects`.
- **Bidirectional probing**: Both sides use `hasObjects` to check which chunk hashes the remote already possesses.

## Core Implementation Files

Three files define the protocol:

1. **[`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts)**: Defines the `SyncRPC` wire contract including method signatures for `push`, `fetchChanges`, `hasObjects`, and watermarks.
2. **[`packages/rpc/src/server.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/server.ts)**: Implements the server-side logic that turns a `Database` into a `SyncRPC` endpoint, handling incoming push streams and change entry application.
3. **[`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts)**: Contains the client-side driver used by computerd containers to orchestrate pull/push loops, watermark reconciliation, and divergence detection.

## Establishing the RPC Connection

The Durable Object initializes the sync server by wrapping its database instance with `createWorkspaceServer`. This function combines the sync server with a shell server to create a `WorkspaceRPC` object that handles both synchronization and command execution.

```typescript
// In the Durable Object (packages/rpc/src/server.ts)
export function createWorkspaceServer(
  db: Database,
  runner: RunnerLike,
  options: ServerOptions = {}
): WorkspaceRPC {
  return new WorkspaceRPCServer(
    createSyncServer(db, options),   // SyncRPC bound to the DO's database
    createShellServer(runner)       // ShellRPC for command execution
  );
}

```

The DO attaches this server to a WebSocket session using `acceptWebSocketSession`, while the computerd container connects to the same WebSocket and receives a proxy implementing `SyncRPC`. The container-side driver in [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) uses this proxy to drive the synchronization loop.

## Reconciliation on (Re)connect

When a connection is established or re-established, the container must reconcile its local watermarks against the Durable Object's state. The `reconcileWatermarks` function in [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) handles this baseline synchronization.

```typescript
// packages/rpc/src/sync-driver.ts
export async function reconcileWatermarks(
  db: Database,
  remote: SyncRPC,
  backend?: string,
): Promise<{ fetchRevReset: boolean; pushRevReset: boolean }> {
  const remoteWatermarks = await remote.watermarks();
  const localFetchCursor = readFetchCursor(db, backend);
  const localPushRev = readWatermark(db, "pushRev", backend);
  // Reset cursors if the remote's log is shorter or it missed pushes
}

```

If the Durable Object's revision log is shorter than the container remembers, the fetch cursor resets to revision 0. If the DO has not seen pushes the container previously made, the `pushRev` watermark resets. This "baseline-from-zero" approach ensures that subsequent `pushOnce` or `pullOnce` calls will safely resend all necessary data.

## The Pull Path via fetchChanges

The container drives the pull side through `pullOnce`, which internally calls `pullOnceImpl`. This function performs a single round-trip synchronization in batches of `PULL_BATCH_SIZE` (256 entries).

The process follows these steps:

1. **Read local fetch cursor**: The driver queries the local database for the last fetched revision using `readFetchCursor`.
2. **Request changes**: The container calls `remote.fetchChanges({ after })`, passing its current cursor. The Durable Object returns a stream containing:
   - `currentCursor`: The DO's current revision snapshot
   - `appliedPushCursor`: The DO's view of what the container has already pushed
   - `stream`: A `ReadableStream<ChangeEntry>` of batched change entries
3. **Detect divergence**: If the DO's `appliedPushCursor` lags behind the container's `pushRev`, or if `currentCursor` is behind the fetch cursor, the driver cancels the stream, resets local watermarks, and retries once.
4. **Batch processing**: For each batch, the driver:
   - Collects needed chunk hashes from file entries
   - Probes the DO with `hasObjects` to identify missing chunks
   - Fetches missing blobs via `fetchObjects` and stages them with `stageBlob`
   - Applies change entries via `applyChanges`
   - Advances the fetch cursor using `writeFetchCursorIfAhead`

After the stream drains, the driver writes the final `currentCursor` as the new fetch cursor, ensuring the next pull starts after the last seen revision.

## The Push Path via push

After pulling, the container pushes local changes via `pushOnce`. This function collects entries since the last `pushRev`, deduplicates chunk hashes, and streams both blobs and metadata to the Durable Object.

```typescript
// packages/rpc/src/sync-driver.ts
export async function pushOnce(
  db: Database, 
  remote: SyncRPC, 
  backend?: string
): Promise<number> {
  const sincePush = readWatermark(db, "pushRev", backend);
  const localRev = currentRev(db);
  if (localRev <= sincePush) return 0;   // Nothing new to push
  
  // Collect entries, probe DO with hasObjects()
  // Stream missing blobs via pushObjects()
  // Stream change entries via push()
}

```

The push workflow:

1. **Collect entries**: Gather all change entries since the last `pushRev` watermark.
2. **Deduplicate chunks**: Probe the Durable Object with `hasObjects` to determine which chunks need uploading.
3. **Upload blobs**: Stream missing chunks via `remote.pushObjects`.
4. **Stream changes**: Send change entries via `remote.push`. The Durable Object applies these transactionally using `applyChangesSync` within a `transactionSync` block (lines 38-41 of [`server.ts`](https://github.com/cloudflare/computer/blob/main/server.ts)).
5. **Verify progress**: The DO returns an `appliedPushCursor` that must cover the sender's `senderRev`. The driver asserts this via `assertAppliedPushCursor`.
6. **Update watermark**: Finally, the driver updates the local `pushRev` to the revision used for the push.

## Full Sync Tick and Loop Suppression

A complete synchronization cycle consists of a **tick**: pulling first, then pushing. The `tick` function in [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) implements this pattern:

```typescript
export async function tick(
  db: Database,
  remote: SyncRPC,
): Promise<{ pulled: ApplyResult; pushed: number }> {
  const pulled = await pullOnce(db, remote);
  const pushed = await pushOnce(db, remote);
  return { pulled, pushed };
}

```

Pulling before pushing prevents loopback—changes the DO just pushed to the container won't be immediately re-pushed in the same cycle. The container typically runs this tick on a timer or event-driven basis to maintain eventual consistency.

## Fault Tolerance and Invariants

The protocol implements several safety mechanisms to handle network failures, restarts, and state divergence:

- **Cross-side watermark divergence**: Detected in `pullOnceImpl` (lines 30-34 of [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts)). The driver resets cursors and retries once; a second divergence throws an assertion error, indicating a protocol break.
- **Idempotent application**: Both `applyChanges` and `applyChangesSync` drop entries whose live state already matches the incoming entry, making retries safe.
- **Chunk deduplication**: Both pull and push sides send only chunks the peer lacks, limiting network traffic to O(batch size).
- **Atomic commits**: The server's `push` handler wraps the entire batch in `transactionSync`, ensuring that change entries and blob references commit atomically.

## Summary

- The sync protocol uses the `SyncRPC` interface to enable bidirectional replication between Cloudflare Durable Objects and computerd containers.
- Three core files implement the protocol: [`interface.ts`](https://github.com/cloudflare/computer/blob/main/interface.ts) defines the contract, [`server.ts`](https://github.com/cloudflare/computer/blob/main/server.ts) provides the DO-side implementation, and [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) drives the container-side logic.
- Watermark reconciliation via `reconcileWatermarks` ensures consistent baselines after reconnections or restarts.
- The pull path uses `fetchChanges` to stream batched entries and `fetchObjects` to retrieve missing blobs, processing in batches of 256.
- The push path uses `hasObjects` to deduplicate chunks, then streams via `pushObjects` and `push`, with the DO applying changes transactionally.
- The `tick` function sequences pull-then-push to prevent loopback and maintain eventual consistency.

## Frequently Asked Questions

### What happens when a computerd container reconnects after a crash?

When a container reconnects, it calls `reconcileWatermarks` to compare its local cursors against the Durable Object's state. If the DO's revision log is shorter than the container remembers, or if the DO missed previous pushes, the container resets its fetch or push cursors to zero. This "baseline-from-zero" approach ensures the next sync cycle safely retransmits all necessary data without corruption.

### How does the protocol prevent sending duplicate data?

Both sides implement chunk-level deduplication using the `hasObjects` method. Before streaming blobs, the sender probes the receiver with a list of chunk hashes. Only the hashes the receiver lacks are sent via `fetchObjects` (during pull) or `pushObjects` (during push). This mechanism ensures network traffic remains proportional to the actual data differences rather than the total dataset size.

### Why does the sync driver pull before pushing in each tick?

The `tick` function deliberately calls `pullOnce` before `pushOnce` to prevent "loopback." If the container pushed first, it might re-send changes that the Durable Object just propagated to it. By pulling first, the container absorbs the DO's latest state, ensuring that only truly local changes—those created since the last pull—are pushed upstream. This ordering is documented in the comment at line 66 of [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts).

### How does the Durable Object ensure data integrity during a push?

The server-side `push` handler in [`packages/rpc/src/server.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/server.ts) wraps the entire change application in a `transactionSync` block (lines 38-41). This guarantees that change entries and their associated blob references commit atomically. If the transaction fails midway, the database rolls back to its previous state, preventing partial writes that could corrupt the SQLite VFS state.