Architecting CRDT Sync for Offline-First Apps

Offline-first architectures fail in a predictable place: the merge step. Two clients edit the same document while partitioned from the network, reconnect, and the naive resolution strategy — last-write-wins on a server timestamp — silently discards one user’s work. At scale, this isn’t an edge case; it’s the dominant failure mode for any distributed system that accepts writes at the edge before establishing consensus. CRDT conflict resolution exists specifically to remove the coordination requirement from that merge step, replacing arbitration with mathematics.
This article covers the architectural decisions behind building a production-grade sync engine using Conflict-Free Replicated Data Types, including primitive selection, causal metadata design, tombstone
#The Divergence Problem in Offline-First Architectures
Consider a mobile field-service application: technicians edit job records on-site with no connectivity, then sync when they re-enter range. Two technicians independently update the same job’s status field. A central server, when it finally receives both writes, has three options: reject one, merge both, or ask a human. Rejecting a write loses data. Asking a human doesn’t scale past a few conflicts a day. Merging requires a data structure that guarantees the same result regardless of the order operations arrive in — this is the exact property CRDTs
The Multi-Cluster Networking Decision
The formal requirement is strong eventual consistency (SEC): given the same set of updates, delivered in any order, any number of times, all replicas converge to an identical state. This is stronger than eventual consistency alone, which only guarantees convergence, not determinism of the merge outcome.
#Why Last-Write-Wins Fails at the Architectural Level
LWW based on wall-clock timestamps introduces two structural problems. First, clock skew across devices — even with NTP, drift of 200-500ms is common on consumer hardware, and offline devices can drift for days. Second, and more fundamentally, LWW collapses a rich edit history into a single winner, which is semantically wrong for anything beyond scalar fields. Concatenated text, ordered lists, and counters all need field-level or operation-level resolution, not document-level arbitration.
Vector clocks

#CRDT Conflict Resolution: Lattice Theory Instead of Timestamps
Every CRDTmerge(a, b) == merge(b, a), merge(merge(a,b),c) == merge(a,merge(b,c)), and merge(a,a) == a. If your merge function satisfies those three properties, you get convergence for free — no coordinator, no consensus round, no quorum write. This is the theoretical basis underpinning most modern architectural patterns for edge-first and multi-region systems.
#State-Based vs Operation-Based CRDT Conflict Resolution
There are two families:
- CvRDTs (convergent/state-based): replicas exchange full state; merge is a pure function of two states. Simple to reason about, but bandwidth cost grows with document size unless you implement delta-state variants.
- CmRDTs (commutative/operation-based): replicas exchange individual operations, which must be delivered with causal ordering (not total ordering) via a reliable causal broadcast layer. Bandwidth-efficient, but the transport layer carries more responsibility.
Most production systems — Automerge, Yjs, Riak’s riak_dt — use delta-state CRDTs, a hybrid that ships only the state delta since the last sync, giving CmRDT-like bandwidth with CvRDT-like merge simplicity.
#Implementation Logic: Building a Sync Engine
#Step 1 — Choosing the Right CRDT Primitive
Primitive selection is the single highest-leverage decision in the whole design. Picking a G-Set where you need an OR-Set means you can never delete an item. Picking an LWW-Register for free-text fields means concurrent edits silently overwrite each other instead of interleaving.
| CRDT Type | Use Case | Metadata Overhead | Convergence Guarantee |
|---|---|---|---|
| G-Counter | Monotonic counters (view counts) | O(n) replicas | Exact sum, no decrement |
| PN-Counter | Increment/decrement counters (stock levels) | 2× G-Counter | Exact sum, supports decrement |
| LWW-Register | Scalar fields (status, priority) | Timestamp + replica ID per field | Deterministic, lossy on true concurrency |
| OR-Set | Tag lists, collaborative membership | Unique tag per add operation (tombstones on remove) | Add-wins semantics |
| RGA | Ordered sequences, collaborative text | Tombstoned linked list, grows with edit history | Deterministic interleaving of concurrent inserts |
#Step 2 — Causal Metadata and Vector Clocks
Every op-based CRDT needs enough metadata to detect concurrency. A vector clock per replica, or a dot-based scheme (unique (replicaId, counter) pairs per operation, as used in delta-CRDTs) is the standard approach. This is where most naive implementations bloat: storing a full vector clock
1interface Dot {
2 replicaId: string;
3 counter: number;
4}
5
6interface LWWRegister {
7 value: T;
8 dot: Dot;
9 timestamp: number; // tie-breaker only, never primary ordering
10}
11
12function mergeLWW(a: LWWRegister, b: LWWRegister): LWWRegister {
13 if (a.timestamp !== b.timestamp) {
14 return a.timestamp > b.timestamp ? a : b;
15 }
16 // Deterministic tie-break on replicaId prevents split-brain divergence
17 return a.dot.replicaId > b.dot.replicaId ? a : b;
18}Note the tie-breaker: relying on timestamp alone for CRDT conflict resolution reintroduces clock-skew risk. The replicaId comparison guarantees a total order even when two writes land in the same millisecond, which happens frequently under batched sync.

#Step 3 — Merge Protocol and Transport
For an OR-Set, adds carry a unique dot and removes carry a tombstone
1{
2 "docId": "job-88213",
3 "field": "tags",
4 "type": "or-set-delta",
5 "adds": [
6 { "value": "urgent", "dot": { "replicaId": "r-mobile-04", "counter": 17 } }
7 ],
8 "removes": [
9 { "dot": { "replicaId": "r-mobile-02", "counter": 9 } }
10 ],
11 "causalContext": {
12 "r-mobile-04": 17,
13 "r-mobile-02": 9,
14 "r-server-00": 44
15 }
16}The causalContext field is what allows a receiving replica to detect whether it has already seen this delta (idempotency) and whether it is missing any prior deltas (causal gap detection) before applying it — critical for correct CRDT conflict resolution under lossy or reordered transports like MQTT or intermittent HTTP long-polling.
Rendering diagram...
#Failure Modes & Edge Cases in CRDT Conflict Resolution
The theoretical guarantees hold only if the implementation respects three invariants: causal delivery, tombstone retention until safe garbage collection, and monotonic dot allocation. Break any of these and convergence silently fails without throwing an exception — the hardest kind of bug to catch in staging.
- Tombstone accumulation: OR-Sets and RGAs never truly delete data at the CRDT layer; they mark it removed. A document edited heavily over two years can carry more tombstone metadata than live content. You need a causal stability protocol — once every replica has acknowledged an operation, and no replica can generate a new concurrent op against it, tombstones can be pruned. This requires tracking a stable version vector across all known replicas, which is operationally expensive for large, high-churn replica sets.
- Dot counter reuse after app reinstall: if a client’s local storage is wiped and the replica ID is regenerated, but the same physical device resumes sending ops with a fresh counter starting at zero, you get silent data corruption if the old replicaId’s ops re-enter circulation. Replica IDs must be immutable and persisted outside app-clearable storage (e.g. Keychain, not localStorage).
- Causal gap stalls: an op-based CRDT that receives operation 12 before operation 11 (out-of-order delivery) must buffer 12 until 11 arrives, or fall back to a full state resync. Under lossy mobile networks this buffering can grow unbounded if the transport has no retry-with-backoff guarantee.
- Merge amplification under high fan-out: a document shared across 500 replicas generates O(n²) potential concurrent-edit pairs during a reconnection storm (e.g. after a regional outage). Batching merges server-side into a single lattice join before broadcasting, rather than pairwise replaying, is mandatory past a few dozen concurrent replicas.
#Scaling and Security Trade-offs
CRDT conflict resolution removes coordination cost but does not remove state cost — that trade-off has to be made explicit to stakeholders before adoption, because the failure signature looks completely different from a traditional locking database.
- Storage growth vs data fidelity: RGA-based text CRDTs retain full edit history as tombstoned nodes. Accepting this overhead buys you real-time collaborative editing with no server-side locking; rejecting it (via periodic snapshot-and-truncate) means you lose the ability to correctly merge very old offline branches.
- Bandwidth vs merge complexity: state-based CvRDTs are simpler to implement and audit but transmit full state on every sync — unacceptable for documents beyond a few hundred KB. Delta-state CRDTs cut bandwidth by 80-95% in typical field-edit workloads but require careful causal-context bookkeeping to avoid double-application.
- Security surface of causal metadata: replica IDs, dots, and vector clocks are typically transmitted unencrypted alongside payload data because the merge server needs to inspect them. This leaks device fingerprinting information (edit frequency, device count per user) unless you rotate replica IDs per session and enforce TLS for all sync transport, per standard CRDT formalism from INRIA’s research on convergent replicated data types.
- Consensus-free writes vs authorisation enforcement: because CRDT conflict resolution deliberately avoids a coordinator, you lose the natural checkpoint where you’d normally enforce write-level authorisation. Access control has to be applied at the operation-generation boundary (client-side, before the op is even created) and re-validated server-side against a signed capability token, not inferred from merge order.
- Multi-region latency vs consistency window: CRDTs give you availability under partition (AP in CAP terms) at the cost of a bounded staleness window between regions. For workloads that need strict linearisability — financial ledgers, inventory reservation — layer a CRDT-backed cache in front of a strongly consistent ledger rather than using CRDTs as the system of record.
None of this makes CRDT conflict resolution a universal replacement for consensus-based replication — it is a targeted tool for the specific shape of problem where availability under partition matters more than immediate global ordering. Field-service tools, collaborative editors, and offline-capable mobile apps sit squarely in that shape. Ledger systems, inventory reservations, and anything requiring a single source of truth at write time do not, and forcing a CRDT model onto them tends to produce the exact class of silent divergence bugs the data structure was designed to eliminate in the first place.
Reader Support: KBY Technologies is an independent engineering editorial. We may earn a commission from affiliate links when you purchase infrastructure, software, or tools through our recommendations. This helps fund our research and labs.
Comments
Add a thoughtful note on Architecting CRDT Sync for Offline-First Apps. Comments are checked for spam and held for moderation before appearing.




