# How the Sync Protocol Handles Concurrent Writes Between Durable Objects and computerd

> Learn how the sync protocol manages concurrent writes between Durable Objects and computerd using revision counters, FIFO serialization, and last-write-wins for conflict resolution.

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

---

**The `@cloudflare/computer` sync protocol uses watermarked revision counters, a single-retry divergence recovery mechanism, and per-workspace FIFO serialization to handle concurrent writes between Durable Objects and `computerd` containers, with last-write-wins resolution for conflicting path mutations.**

The sync protocol in the `cloudflare/computer` repository coordinates state between **Durable Objects (DOs)** and **computerd containers** through a bidirectional synchronization driver. Understanding how this protocol manages concurrent writes is essential for building reliable applications on the platform.

## Watermark-Based State Tracking

The protocol relies on three independent revision counters persisted in SQLite via `@cloudflare/dofs`. These watermarks form the foundation of conflict detection:

| Watermark | Owner | Purpose |
|-----------|-------|---------|
| `pushRev` | Durable Object | Tracks the last DO-side revision pushed to the container |
| `fetchCursor` | Durable Object | Resume point for the container's pull (last `(rev, path)` streamed) |
| `appliedPushCursor` | Container | Echoed back to verify the remote has received pushed changes |

These watermarks are the **only state** the protocol uses to detect and recover from races between sides.

## Pull Path: Detecting and Resetting Divergence

When a container initiates sync via `pullOnce`, the driver fetches the remote's current cursor and the DO's `appliedPushCursor`. If divergence is detected, the protocol executes a single reset-and-retry cycle.

### Divergence Conditions

The driver checks for two failure modes in [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts):

```ts
// pullOnce implementation – watermark divergence handling
// https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts#L30-L38
if (!retried && (pushDiverged || fetchDiverged)) {
  await fetchResult.stream.cancel().catch(() => {});
  console.debug("[pullOnce] cross-side watermark divergence …");
  if (pushDiverged) writeWatermark(db, "pushRev", 0, backend);
  if (fetchDiverged) writeFetchCursor(db, { rev: 0, path: null }, backend);
  return pullOnceImpl(db, remote, backend, true);
}

```

- **`pushDiverged`** — The DO's `appliedPushCursor` lags behind the local `pushRev`. The DO "forgot" a push the container believes succeeded.
- **`fetchDiverged`** — The remote's cursor trails the local `fetchCursor`. The DO lost log entries the container already processed.

A second divergence after retry triggers a **fatal protocol assertion**, preventing infinite loops while handling normal restart scenarios.

## Push Path: Enforcing the Cross-Side Invariant

The push direction maintains consistency through strict cursor validation. Before advancing local watermarks, the driver verifies the remote acks the pushed revision:

```ts
// pushOnce implementation – invariant check
// https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts#L54-L58
assertAppliedPushCursor(response.appliedPushCursor, { rev: localRev, path: null });
writeWatermark(db, "pushRev", localRev, backend);

```

**Failure here aborts the sync** and surfaces corruption rather than silently losing data. This invariant ensures the DO and container cannot diverge on acknowledged writes.

## Serializing Concurrent Sync Calls

While watermark handling addresses state divergence, the protocol also serializes simultaneous sync operations through a **per-workspace tail-promise FIFO**.

> "`Workspace.push()` and `Workspace.pull()` go through a per-Workspace tail-promise FIFO. Two concurrent callers queue — the second can't enter `pushOnce`/`pullOnce` until the first resolves or rejects."  
> — [[`docs/02_sync_protocol.md`](https://github.com/cloudflare/computer/blob/main/docs/02_sync_protocol.md)](https://github.com/cloudflare/computer/blob/main/docs/02_sync_protocol.md#L91-L98)

This FIFO guarantees **ordering of sync operations** but does **not prevent concurrent mutations to the same path** between ticks. For truly concurrent writes, the protocol follows a **last-write-wins** rule.

## Last-Write-Wins Conflict Resolution

When multiple containers mutate identical paths between sync ticks, the outcome is deterministic but lossy:

```ts
// Two containers writing the same file concurrently
// Container A
await workspace.fs.writeFile("/shared.txt", "A version");
await workspace.push();   // pushes A's change

// Container B (runs in parallel)
await workspace.fs.writeFile("/shared.txt", "B version");
await workspace.push();   // pushes B's change; DO keeps the last push

// After both pushes, a third container pulls:
await workspace.pull();   // pulls the DO's current state → contains "B version"

```

The pull-then-push tick order (`pull → push`) ensures each side first incorporates remote changes, but the final state reflects whichever push the DO processes last. Earlier writes are silently overwritten with no merge or error.

## Protocol Guarantees and Failure Modes

| Scenario | Protocol Behavior |
|----------|-----------------|
| Normal restart (DO or container) | Watermarks persist on DO side; other side resets to 0, triggering full re-sync from revision 0 |
| Watermark divergence | Single reset-and-retry attempt; fatal error on repeated divergence |
| Concurrent container writes to same path | Last-write-wins; pull-then-push ordering reduces but does not eliminate conflicts |
| Concurrent sync API calls | FIFO serialization ensures one `pullOnce`/`pushOnce` executes at a time per workspace |

## Handling Watermark Recovery in Practice

Applications should anticipate divergence recovery, particularly after DO restarts:

```ts
// Handling watermark divergence (e.g., DO restart)
try {
  await workspace.pull();   // pullOnce detects fetchDiverged → resets fetchCursor to 0
} catch (e) {
  console.error("Sync failed:", e);
}

```

The automatic retry typically resolves transient divergence. Persistent failures indicate protocol corruption requiring investigation.

## Summary

- **Watermark counters** (`pushRev`, `fetchCursor`, `appliedPushCursor`) track sync state in SQLite and enable divergence detection
- **Single-retry recovery** resets divergent watermarks to 0, allowing re-sync from scratch without infinite loops
- **FIFO serialization** prevents concurrent `pushOnce`/`pullOnce` calls on the same workspace
- **Last-write-wins** resolves path conflicts when multiple containers write concurrently between sync ticks
- **Invariant assertions** on `appliedPushCursor` guarantee acknowledged writes cannot be silently lost

## Frequently Asked Questions

### What happens when both a Durable Object and a container restart simultaneously?

The Durable Object's watermarks survive in SQLite via `@cloudflare/dofs`, while the `computerd` container resets its watermarks to 0 on restart. When the container reconnects, `pullOnce` detects the divergence (its `fetchCursor` of 0 trails the DO's actual cursor), resets its local `fetchCursor` to 0, and retries once. A full re-sync from revision 0 re-establishes consistency.

### Can two containers corrupt data by writing to the same file at the same time?

The protocol prevents corruption through last-write-wins semantics, not through locking. Both containers can succeed in their `push()` calls, but the DO retains only the later-received write. No error is raised for the overwritten change. Applications requiring merge semantics must implement their own coordination layer above the sync protocol.

### Why does the protocol allow only one retry for watermark divergence?

The single-retry limit in `pullOnceImpl` prevents infinite loops when a genuine protocol bug or storage corruption causes persistent divergence. After one reset-and-retry cycle, a second divergence triggers an assertion failure, surfacing the problem for investigation rather than masking it with repeated retries.