How the Sync Protocol Handles Concurrent Writes Between Durable Objects and computerd
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:
// 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'sappliedPushCursorlags behind the localpushRev. The DO "forgot" a push the container believes succeeded.fetchDiverged— The remote's cursor trails the localfetchCursor. 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:
// 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()andWorkspace.pull()go through a per-Workspace tail-promise FIFO. Two concurrent callers queue — the second can't enterpushOnce/pullOnceuntil the first resolves or rejects."
— [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:
// 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:
// 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/pullOncecalls on the same workspace - Last-write-wins resolves path conflicts when multiple containers write concurrently between sync ticks
- Invariant assertions on
appliedPushCursorguarantee 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.
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 →