# How Cap'n Web RPC Sync Protocol Handles Conflict Resolution Between Durable Objects and Containers

> Learn how the Capn Web RPC sync protocol uses last-write-wins to resolve conflicts between Durable Objects and containers, ensuring the latest changes always prevail.

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

---

**The Cap'n Web RPC sync protocol resolves conflicts using a last-write-wins strategy, where the most recent batch of changes arriving at the Durable Object overwrites any earlier state for the same path.**

The sync protocol connects **Cloudflare Durable Objects (DOs)** with container-side FUSE mounts through a state-based, bidirectional synchronization system. Unlike traditional distributed systems that attempt to merge concurrent edits, the **computer** repository's implementation deliberately avoids complex conflict resolution in favor of deterministic simplicity. This article examines exactly how conflict resolution works at the protocol level, using source code references from the official implementation.

## Core Architecture: State-Based Sync with Watermarks

The foundation of conflict handling rests on **monotonic revision counters** exchanged between the DO and container. Each side maintains its own `rev` counter and communicates progress through watermarks on every push and pull operation.

In [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts), the Cap'n Web RPC interface defines the core methods: `push`, `fetchChanges`, `hasObjects`, `fetchObjects`, and related helpers. These enable the bidirectional incremental sync described in `docs/02_sync_protocol.md#watermarks`.

The protocol enforces a critical ordering guarantee: **pull-before-push**. On every sync tick, the container first pulls remote changes, then pushes local ones. This sequencing ensures a writer never propagates stale state by overwriting remote changes it hasn't yet observed.

## How Conflicts Are Defined and Detected

The protocol's definition of a conflict is straightforward: two writers mutate the same path without seeing each other's change first. However, **no explicit conflict detection occurs**. As documented in `docs/02_sync_protocol.md#conflict-semantics`:

> "The protocol does not detect such a situation; it simply applies the later batch."

This design choice eliminates the need for distributed consensus or operational transformation. The receiver reconciles incoming `ChangeEntry` records against its current live state declaratively, with no rename opcodes or operation history required.

## Last-Write-Wins Resolution in Practice

When concurrent writes occur, resolution follows this deterministic path:

1. **Container A pushes first** — The DO stores revision `rev = 42` for path [`/shared/config.json`](https://github.com/cloudflare/computer/blob/main//shared/config.json)

2. **Container B pushes second** — Before A's change is pulled elsewhere, B writes the same path and pushes, the DO stores `rev = 43`

3. **Subsequent pulls receive B's state** — The batch arriving last (B's) overwrites A's version permanently

The [`packages/rpc/src/server.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/server.ts) implementation handles this server-side, applying received changes atomically within SQLite transactions. Each batch in [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) is idempotent: partial failures trigger complete rollback, guaranteeing no half-applied state.

## Code Example: Safe Sync Pattern with Pull-Before-Push

```typescript
import { Workspace } from '@cloudflare/computer';

// 1️⃣ Always pull remote state before local modifications
await ws.pull();

// 2️⃣ Perform local write in container-side FUSE mount
await ws.fs.writeFile('/agent/state.json', JSON.stringify({ count: 1 }));

// 3️⃣ Push changes to Durable Object
await ws.push();

// 4️⃣ Pull again to absorb any concurrent changes
await ws.pull();

```

The explicit `pull()` calls bracketing local operations minimize conflict windows by ensuring the container works from the latest known state.

## Code Example: Simulating Concurrent Container Writes

```typescript
// Container A -----------------------------------------------
await wsA.fs.writeFile('/shared/config.json', '{"value":"A"}');
await wsA.push();  // Creates rev 101

// Container B (concurrent execution) ------------------------
await wsB.fs.writeFile('/shared/config.json', '{"value":"B"}');
await wsB.push();  // Creates rev 102 (arrives later)

// Final state: both containers see "B" after pull
await wsA.pull();  // Returns '{"value":"B"}' — last write wins

```

This demonstrates the protocol's behavior when coordination is absent: no error, no merge, simply the most recent successful push surviving.

## Why the Design Omits Merge Logic

The **computer** repository's maintainers chose last-write-wins for three reasons, per `docs/02_sync_protocol.md#alternatives-considered`:

- **Simplicity and determinism** — Final-state wire format eliminates ordered replay requirements
- **Crash robustness** — Watermarks and idempotent batch application guarantee consistent recovery
- **Performance** — Per-path coalescing and chunk-level deduplication minimize data transfer

The wire protocol carries only adds, updates, and tombstones — no operational history. Receivers reconcile declaratively against their current state, reducing both complexity and message size.

## Practical Mitigation Strategies

Since the protocol provides no merge facilities, applications must implement coordination:

- **Single-writer pattern** — Maintain only one active writer per workspace, with explicit hand-offs using `workspace.pull()` before transitions
- **Namespace partitioning** — Assign dedicated subtrees per agent (`/agent-a/`, `/agent-b/`) to eliminate path collisions
- **DO-side authoritative state** — Treat workspace files as ephemeral scratch space; persist critical shared state through the Durable Object's RPC surface rather than the sync protocol

## Summary

- **Conflict resolution strategy**: Unconditional last-write-wins at batch granularity
- **Key ordering guarantee**: Pull-before-push prevents stale overwrites of observed state
- **No explicit conflict detection**: Concurrent edits to the same path silently overwrite earlier versions
- **Atomic application**: SQLite transactions in [`packages/rpc/src/server.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/server.ts) ensure batch-level consistency
- **Revision tracking**: Monotonic counters and watermarks enable resumable, idempotent synchronization

## Frequently Asked Questions

### Does the Cap'n Web sync protocol detect when two containers edit the same file simultaneously?

No. The protocol does not detect or flag concurrent modifications. Per `docs/02_sync_protocol.md#conflict-semantics`, conflicts are defined but not detected — the later arriving batch simply overwrites the earlier one without warning or merge attempt.

### What prevents a container from losing its changes when another container pushes?

Nothing at the protocol level. The **pull-before-push ordering** in [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) only ensures a container doesn't overwrite remote state it hasn't seen. To prevent loss, applications must coordinate externally or use namespace partitioning to avoid overlapping write paths.

### Are the sync operations actually atomic?

Yes, at the batch level. As documented in `docs/02_sync_protocol.md#failure-handling`, each batch applies inside a SQLite transaction. If any entry in a batch fails, the entire batch rolls back, preventing partial state application and enabling safe retries.

### Can I implement custom merge logic on top of this protocol?

You can, but not within the sync protocol itself. The recommended approach per the documentation is to treat workspace files as scratch space and implement shared mutable state through direct DO RPC calls outside the FUSE sync path, where your application controls serialization and merging.