CRDTs Explained: How Offline Devices Sync Without a Server Deciding Truth
How conflict-free replicated data types let devices sync edits without a central server - the convergence math, LWW registers, and garbage collection.
Two colleagues take the same project file onto different flights. Over Denver, one reorders its sections; over Reykjavík, the other rewrites an introduction. Both devices are offline, so neither sees the other’s work, and by the time they land there are two legitimate versions of the document—and no server anywhere to declare a winner. Every collaboration tool that works offline eventually collides with this moment. The interesting question is not whether copies diverge but what happens when they meet again.
Conflict-free replicated data types—CRDTs—are one family of answers. The idea is deceptively simple to state and mathematically demanding to get right: design your data structures so that any two replicas that have seen the same set of edits end up in exactly the same state, regardless of the order the edits arrived in, regardless of duplicates, no coordinator required. This article builds up that property from first principles, walks through a working last-write-wins register with hybrid logical clocks, and ends where every honest CRDT discussion should—with tombstones, metadata growth, and what production libraries actually do about them.
It is the sync-side companion to our earlier explainer on local-first software; read that one for the motivation, this one for the machinery.
Why server-sequenced sync breaks offline
Google Docs popularized a synchronization technique called operational transformation (OT). Each user’s keystrokes become operations, and when two operations were generated against the same state concurrently, a transformation function rewrites each one so it makes sense applied after the other. The catch sits quietly in that sentence: transformation results depend on the order operations are processed, so everyone must agree on one order. In practice that agreement comes from a central server that sequences every operation as it arrives.
That architecture suits always-online editors fine. But the sequencer assumption collapses the moment connectivity does:
- No arbiter while disconnected. A device offline cannot ask which concurrent operations came first, so it cannot transform incoming work against its own—it can only guess, and guesses differ per device.
- Ordering is not recoverable later. Replaying a weekend of queued edits against a week of everyone else’s edits produces a combinatorial space of transformations nobody wants to implement twice, let alone verify.
- The failure you care about is convergence, not ordering. After the dust settles, every replica must hold the same document. OT achieves this by controlling order; the offline case demands a method that does not need one at all.
So the requirement gets restated: find data types whose merge operation gives the same result under any delivery order, tolerates duplicate deliveries, and never needs a coordinator. Three algebraic properties deliver exactly that.
The algebra of agreement
Take ∘ to be the operation that merges two states. If ∘ satisfies three properties, merge order stops mattering:
| Property | Definition | What it buys |
|---|---|---|
| Commutativity | a ∘ b = b ∘ a | Arrival order is irrelevant |
| Associativity | (a ∘ b) ∘ c = a ∘ (b ∘ c) | Batching and forwarding are safe |
| Idempotence | a ∘ a = a | Duplicate deliveries are harmless |
Each has an everyday instance. Set union is commutative—merging {alice, bob} with {bob, carol} yields the same trio either way. Addition is associative, which is why summing partial tallies in any grouping works. And union is idempotent: merging a set with itself changes nothing, which is precisely why retransmitting a stale copy after a flaky sync is safe instead of corrupting.
A structure whose merge function has all three properties forms what the literature calls a join semilattice: any pair of states has a unique least upper bound, their merged result. Shapiro and colleagues formalized the CRDT families around this observation in their widely cited 2011 paper; the practical takeaway survives translation out of the mathematics—if your state only grows along well-defined merge rules, replicas converge without anyone coordinating.
One further property earns explicit mention because every durable design leans on it: monotonicity—merged states only grow, information accumulates, and nothing a replica once knew is forgotten through merging alone. Deletions therefore cannot mean physical removal; they must become annotations (the tombstones examined later). That single constraint is what makes convergence provable instead of merely observed, and it is also the quiet source of the storage costs every long-lived CRDT eventually pays.
State-based versus operation-based
The literature splits CRDTs along a transmission axis, and the split maps directly onto engineering trade-offs.
State-based CRDTs (CvRDT) ship entire states between replicas. Merging is the semilattice join described above. Because states are idempotent when merged, transport quality becomes irrelevant—messages may arrive late, duplicated, or not at all, and eventual gossip over any channel still converges. The price is bandwidth: a long-lived document accumulates history, and shipping all of it repeatedly is wasteful.
Operation-based CRDTs (CmRDT) ship the operations themselves—“insert ‘e’ at position 12”—assuming exactly-once delivery against every replica. Messages stay tiny, but the delivery guarantee is a hard requirement: lose or duplicate one op and replicas silently drift apart forever.
Most real systems live pragmatically in between, shipping deltas: since the last acknowledged exchange, only the new pieces of state travel. Deltas preserve the forgiving semantics of state-based merging while keeping messages proportionate to recent activity.
| State-based (CvRDT) | Operation-based (CmRDT) | |
|---|---|---|
| Message contents | Full (or delta) state | Individual operations |
| Transport requirements | Any—lossy, duplicated, reordered | Exactly-once, in causal order |
| Bandwidth profile | Higher baseline, deltas mitigate it | Minimal per edit |
| Typical fit | Peer-to-peer sync, unreliable networks | Central relay with delivery guarantees |
For browser applications—where a service worker might sync opportunistically through whatever network appears—the forgiving side of the table is usually worth its bytes.
Code walkthrough: a last-write-wins register with hybrid clocks
The smallest useful CRDT is the LWW-register: one slot holding one value, where concurrent writes are resolved by comparing timestamps and letting the latest win. It is the conflict-resolution core inside many key-value layers, presence systems, and preference stores.
Naively, Date.now() would provide timestamps—but wall clocks lie. Two devices rarely agree on the current time, and one clock jumping forward can permanently dominate every future edit made elsewhere. Hybrid logical clocks (HLC) tame this by pairing a wall-clock reading with a monotonic counter and a node identifier, yielding stamps that track real time closely yet remain strictly ordered even across skewed devices.
Here is a complete register in TypeScript:
type Stamp = { wall: number; count: number; node: string };
// Total order over stamps: wall time first, then counter, then node id.
// The node id breaks exact ties, so two stamps can never be equal.
function later(a: Stamp, b: Stamp): boolean {
return a.wall !== b.wall ? a.wall > b.wall
: a.count !== b.count ? a.count > b.count
: a.node > b.node;
}
export class LwwRegister<T> {
private stamp: Stamp = { wall: 0, count: 0, node: "" };
private value?: T;
constructor(private readonly nodeId: string) {}
get(): T | undefined {
return this.value;
}
// Local write: tick the clock forward, then record value and stamp.
set(next: T): void {
const wall = Date.now();
const s = this.stamp;
this.stamp = wall > s.wall
? { wall, count: 0, node: this.nodeId } // clock moved: reset counter
: { wall: s.wall, count: s.count + 1, node: this.nodeId }; // same ms: bump counter
this.value = next;
}
// Remote state arrived: keep whichever stamp sorts higher. Order-independent,
// idempotent (merging the same state twice changes nothing), commutative.
merge(remoteValue: T, remoteStamp: Stamp): void {
if (later(remoteStamp, this.stamp)) {
this.value = remoteValue;
this.stamp = remoteStamp;
}
}
}
Three details carry the correctness, and each deserves a slow read.
The tie-breaking chain makes the order total. Wall time alone leaves ties—two devices writing in the same millisecond. The counter separates writes within one millisecond on one device; the node id separates devices. With no possible ties, later defines a strict total order, so every replica examining the same two stamps elects the same winner. That is convergence, mechanically.
merge is deliberately symmetric and blind. It never asks who wrote first in human terms, never consults a clock on arrival, never special-cases duplicates. Feed it the same set of states in any permutation and the surviving value is identical—which is exactly the commutativity-plus-idempotence contract from earlier.
Real HLCs also advance on receive. Production hybrid clocks bump their local stamp when a remote stamp arrives, preserving causality chains across multi-hop relays. The toy above keeps receive passive for clarity; adding a tick() call inside merge is the one-line upgrade toward the full algorithm.
It helps to trace the flight-delay scenario through this machinery. Both colleagues edit offline, each set() stamping writes with its own wall clock and node id. When connectivity returns, devices exchange final states; every replica runs merge against the other’s value, the later stamp wins identically on both sides, and the losing edit is gone—deterministically, identically, everywhere. Nobody negotiated and nobody waited. The cost of that automation is real (one edit vanished silently), which is precisely why field-level registers exist: shrink a register’s scope and the blast radius of any lost write shrinks with it.
What LWW cannot do is preserve both edits. Someone’s text genuinely disappears. That is the deal: registers buy simplicity by choosing a winner. Structures like grow-only sets, counters, and sequence CRDTs (the engines behind collaborative rich text in libraries such as Yjs and Automerge) spend more metadata to merge contributions instead of picking winners—same algebra, richer payloads.
Tombstones: the price of remembering
Deletes create the awkward problem. A replica cannot simply erase an element, because another offline device may still hold an older state containing it—and when the two sync, erasure must beat resurrection. The standard answer is the tombstone: deletion marks the element dead rather than removing it. The element stays in the metadata forever, contributing to every state transfer.
On a note-taking app used for a year, tombstones plus per-edit identifiers can outweigh the live content several times over. Left unmanaged, the CRDT becomes a museum of everything ever typed. The honest mitigation strategies:
- Epoch compaction. Once every replica has acknowledged seeing a change, its tombstone serves no purpose. Periodically pin a GC anchor—a state every known device has passed—and drop history below it. Devices joining later bootstrap from the anchor.
- Scoped lifetimes. Comments, cursor presence, and notification objects often deserve TTLs rather than immortality; not every collaborative datum needs archival semantics.
- Delta shipping with anti-entropy windows. Full-state gossip only among peers that have been away; steady-state partners exchange small deltas, bounding routine bandwidth regardless of total history.
This is where the toy meets production. Yjs and Automerge exist largely to industrialize these unglamorous parts—compact encodings, binary deltas, aggressive garbage collection—on top of the same convergence algebra. The mathematics is the easy half of a CRDT; the storage discipline is the engineering half.
Questions people often ask
Are CRDTs winning over OT?
For decentralized and offline-first systems, mostly yes—no central sequencer means no single point of failure and no transformation-function proofs to maintain. Google Docs-style OT remains perfectly sound for centrally hosted, online-first editing, where its lower memory overhead is attractive. The decision axis is topology: server-authoritative favors OT, peer-symmetric favors CRDTs.
Can CRDTs handle rich text?
Yes—sequence CRDTs assign each character (or block) a stable, globally unique identifier, so concurrent insertions interleave deterministically without positional renumbering. The identifier bookkeeping is why collaborative text engines carry more metadata than plain strings, and why tombstone management matters most in editors. What sequence CRDTs cannot merge gracefully is taste: two users bolding overlapping selections resolve structurally, but the result may match neither intent. Rich text converges; editorial judgment does not commute.
What does this mean for privacy?
Everything the CRDT ships—states, deltas, tombstones—is readable by whatever transports them. Convergence math provides integrity, not confidentiality. End-to-end encryption of payloads restores privacy, and the mechanics are covered in our Web Crypto walkthrough; a fuller inventory of sync-related exposures lives in the everyday threat model worksheet.
Do I still need a server?
Not for correctness—that is the point. A mailbox that ferries opaque envelopes between devices (a dumb relay, a git repository, AirDrop, QR codes) suffices for convergence. Servers become worthwhile for availability and convenience: relaying when peers are rarely online simultaneously, backing up states, and pushing updates. They stop being authorities, which is a quieter job description.
The takeaway
CRDTs replace coordination with algebra. Design merge to be commutative, associative, and idempotent, stamp edits with clocks that cannot tie, and replicas converge no matter how chaotically messages arrive—no sequencer, no locks, no consensus round-trips. Start from registers for independent fields, graduate to sets, counters, and sequence structures as your data demands richer merging, and budget honestly for the metadata growth that deletes incur. Local persistence is the other half of the story—how IndexedDB actually works covers where those states sleep between syncs.
Share this article
Link, preview card, or your favorite app
Instagram has no web share link — save the card, copy the caption, post them together.