Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

Why CRDTs exist

Two people typing into the same document at the same time, possibly offline, possibly across the world. The merge has to come out the same on both screens with no central referee. CRDTs are the data structures that make that arithmetic instead of a fight.

Data intermediate Apr 29, 2026 · updated Aug 25, 2026 · 14 min read

On this page

The picture version

Six pictures for a reader who has never heard of a CRDT, following one instant: you and I typing into the same spot in the same sentence, at the same moment, with neither laptop having heard from the other.

1 · The problem

Two edits, one spot, and no referee.

the cat sat you type ‘X’ here your laptop the cat sat I type ‘Y’ here my laptop × neither has heard from the other yet Both screens still have to end up showing the same sentence. no server picks an order, no locking, nobody waits for permission before typing and it has to be instant, automatic and silent — there is no “resolve conflict” button
Every design decision in the post is about this instant. The demand is that any two replicas which have seen the same set of edits end up identical, whatever order those edits arrived in.

2 · The naive way

“Take the newer document” converges — and eats a paragraph.

“send my whole document; you take the newer one” yours, 10:03 mine, 10:04 result: mine wins older newer It converges. One of us just lost an entire paragraph. “converges” and “correct” are different words — and that gap is where CRDTs live
Last-write-wins passes the convergence test and fails the only test that matters. The fix is to stop replacing and start combining: make the merge a function of both values instead of a choice between them.

3 · The first fix

A merge that can’t care who went first.

don’t replace — combine, with a merge that obeys three rules commutative merge(a, b) = merge(b, a) associative merge(a, merge(b, c)) = merge(merge(a, b), c) idempotent merge(a, a) = a so it doesn’t matter which replicas you merge, in what order, or how many times the textbook case: a grow-only counter Replica A: [3, 0, 1] Replica B: [0, 2, 1] merge: [3, 2, 1] each replica only ever increments its own slot merge takes the elementwise maximum any order, any number of times — the same answer
Those three properties are doing all the work: you can gossip full state around, drop messages, deliver them out of order and redeliver them, and the replicas still agree. But you are editing a document, not a counter — shipping the whole state on every keystroke is fine for a three-slot vector and absurd for a novel.

4 · Where it really breaks

“Position 5” is a lie the moment someone else types.

“insert ‘X’ at index 5” — ship the operation, not the whole document the·cat· 01234567 both of us aim at index 5 my insert arrives first the·cYat· 012345678 Your index 5 now points at a different character. your operation was written against a sentence that no longer exists Positions in a shared sequence are not stable — so operations defined against them do not commute.
This is the moment collaborative text editing gets genuinely hard, and it is the instant from the hook. The algebra was never the obstacle; the addressing was.

5 · The fix, and its bill

Give every character a name that never moves.

instead of “insert at index 5”: “insert this character here” the·cat· a1a2a3a4a5a6a7a8 every character carries a globally unique id ids never move, so your operation always has somewhere to land you insert X after id a4; I insert Y after id a4 — and both replicas interleave them by the same rule … a4 → X → Y → a5 … on both screens and here is the bill delete a character and a concurrent insert anchored to it has nowhere to land — so you don’t remove it, you mark it dead. Those markers are tombstones. they accumulate with edit history rather than document size, and collecting them safely means proving no live replica can still reference them — the one thing this whole design was built to avoid needing.
Different CRDTs assign those identifiers differently — dense fractional ids, or naming the element an insert sits after, as drawn here — but the move is shared: identify elements by stable identity instead of by mutable position. That is also what lets the model transfer from text to any other ordered data.

6 · Keep this card

The whole thing on one index card.

CRDT = a combine rule that ignores order and timing + per-element identity, so a sequence can obey it + the tombstones that identity forces on you ∴ write anywhere, anytime, ask nobody it converges to a state — not necessarily the one you meant
Picture to keep: two people separately adding coins to two piles, then pouring the piles together — it doesn’t matter who pours first, or whether someone pours the same pile twice. Where it breaks: a document has an order and coins don’t, and making “where does this character go” survive the pour is the hard part the analogy can’t show you.

Why it exists

Open a shared doc with a colleague. You’re both typing into the same paragraph. Sometimes one of you is on a flaky train. The cursors move, the text rearranges, and the final document looks the same on both laptops. Nobody fights, nobody loses a sentence, nobody has to click “resolve conflict.” Keep one specific instant of that in mind for the rest of this post — you and I both typing a character into the same spot in the same sentence, at the same moment, with neither laptop having heard from the other yet. Every design decision below is about that instant.

Now try to write the code that does this.

The naive plan — “send each keystroke to a server, the server picks an order, everyone replays” — falls over the moment one of you is offline, or the server is far away. The slightly-less-naive plan — “diff the two documents, three-way merge them like Git does” — works fine for source control where humans review the result, and works terribly for a live cursor where the merge has to be instant, automatic, and silent.

What you actually want is something stronger: a way to represent the document such that any two replicas that have seen the same set of edits end up identical, no matter what order the edits arrived in. No central authority, no locking, no asking permission before typing. Edits commute.

That property — concurrent edits always merge to the same answer — is the whole reason Conflict-free Replicated Data Types (CRDTs) were invented. They are the data structures that make collaborative editing arithmetic rather than negotiation.

Why it matters now

Real-time multiplayer is no longer a Google-only superpower. Multi-cursor canvases, notes apps that sync across devices, editors that keep working on a train and reconcile afterwards — all of them need some automatic merge, and whichever mechanism they picked, they were solving the problem CRDTs are designed for.

Being precise about who uses what is harder than the marketing suggests, and most public claims here are secondhand. Two that aren’t: Figma’s own engineering blog describes their multiplayer system as CRDT-inspired and explicitly says it isn’t using true CRDTs, and Google Docs is long and widely described as built on OT, the older approach. Yjs and Automerge are two of the most visible open CRDT implementations and underpin a long tail of local-first apps. Beyond that, what most named products run internally simply isn’t published — the honest summary is that the problem is universal while the mechanism is usually undisclosed.

The shape recurs beyond documents, too: any multi-writer replicated state with no referee — a multi-device to-do list, a shopping cart, a counter gossiped between regions — has the same problem. Whether a given team reaches for a CRDT library or hand-rolls something simpler is mostly anecdote; the problem is not.

The short answer

CRDT = data structure + a combine rule that ignores order, duplication, and timing

Picture to keep: two people separately adding coins to two piles, then pouring the piles together. It doesn’t matter who pours first, or whether someone pours the same pile twice — the heap on the table is the same. Like pouring coins, except a document has an order and coins don’t: the hard work in a text CRDT is making “where does this character go” survive the pour, and that’s the part the analogy can’t show you.

In English: every operation can be applied in any order, applied more than once, or applied alongside someone else’s operation, and the final state is the same. The merge is forced to be safe by the shape of the data, not by a coordinator deciding who wins. (The compression line is deliberately loose about where that rule lives: in the state-based flavor below it’s a merge function on values that is commutative, associative, and idempotent; in the operation-based flavor it’s the operations themselves that must commute. Same demand, two places to satisfy it.)

How it works

Two flavors exist, and it’s tempting to present them as a menu. Start from the failure instead: newer-wins loses edits, shipping whole state is too heavy for text, and raw positions don’t survive concurrent inserts. Each flavor answers one of those breakages — so build it that way, with you and me typing into the same sentence.

Naive attempt: send my whole document, you take the newer one. Simple, and it converges: last write wins. Why it breaks: one of us loses an entire paragraph. “Converges” and “correct” are different words, and this is the gap CRDTs live in.

Fix: don’t replace, combine — make the merge a function of both values. Now you need a merge that gives the same answer regardless of who merges what, in what order, how many times. That’s a real algebraic constraint, and it’s the first flavor.

State-based (CvRDT) — ship the whole value, take the join

Each replica holds a value drawn from a join-semilattice: the values can be combined by a merge operation that is commutative (merge(a, b) = merge(b, a)), associative (merge(a, merge(b, c)) = merge(merge(a, b), c)), and idempotent (merge(a, a) = a).

Those three properties are doing all the work. Together they mean: it doesn’t matter which replicas you merge, in what order, or how many times — the result is the same. You can ship full state around with a gossip protocol, drop messages, deliver them out of order, redeliver them, and the system still converges.

This is the clean algebraic version of the idea — and notice how far it already is from our shared sentence. The textbook example is a G-Counter (grow-only counter): each replica keeps its own slot in a vector of counts, only ever increments its own slot, and merge takes the elementwise maximum. Two replicas that have seen different increments will, after exchanging state, agree on every slot — and therefore agree on the sum.

Replica A: [3, 0, 1]   (A=3, B=0, C=1)
Replica B: [0, 2, 1]
merge:     [3, 2, 1]   (max in each slot)

A PN-Counter (supports decrements) is two G-Counters glued together. A G-Set (grow-only set) just unions. An OR-Set (observed-remove set) tags each insert with a unique ID so you can tell “I’m removing the banana I saw” from “I’m removing all bananas, including ones added later.” Once you start composing these, you can build registers, maps, and sequences.

Why it breaks: you are not editing a counter, you are editing a document. Shipping the whole state on every keystroke means shipping the whole document on every keystroke — fine for a three-slot vector, absurd for a novel.

Operation-based (CmRDT) — ship the operations

Fix: send only what changed. Replicas broadcast each operation (“insert ‘x’ at position p with ID k”) instead of the full value. For this to converge without a total order, every pair of concurrent operations must commute — the same algebra as before, relocated from the merge function into the operations themselves.

Why it breaks: “position p” is a lie. If I insert at index 5 and you insert at index 5, and my edit arrives first, your index 5 now points at a different character. Positions in a shared sequence are not stable, so operations defined against them do not commute. This is the moment collaborative text editing gets genuinely hard, and it’s the instant from the hook.

Fix: give every character an identity that never moves. Instead of “insert at index 5,” an operation says “insert this character here,” where here is an identifier that other people’s inserts cannot invalidate.

The general move: each character gets a globally unique identifier that places it in some well-defined order, and concurrent inserts at “the same spot” produce different identifiers that the data structure knows how to interleave deterministically. Different CRDTs do this differently, but they share one move: identify elements by stable identity instead of by mutable position, which is what lets the model transfer from text to any other ordered data. Logoot and Treedoc use dense fractional identifiers (a path in a tree, or a list of (replica-id, counter) pairs) so a new ID can always be found between two neighbors; RGA takes a different approach, treating the sequence as a growable linked structure where each insert names the element it sits after. Per their own documentation, Yjs adapts an algorithm called YATA, while Automerge’s list type is RGA-based and its rich-text type uses a newer scheme called Peritext. The family is less uniform than introductory writing usually implies.

Why it breaks: you can’t delete anything. If I remove a character while your concurrent insert names it as its anchor, your operation has nowhere to land. Fix: don’t actually remove it — mark it dead. Which brings us to the bill.

What CRDTs cost

This is the seams part. The math is beautiful; the engineering is real.

Back to the opening instant: you and I typing into the same spot in the same sentence, neither laptop having heard from the other. Both characters get globally unique identifiers, both operations name the character they sit after, and both replicas interleave them by the same deterministic rule — so both screens land on the same sentence, without either of us waiting for a referee. That’s the whole answer.

You started with CRDT = data structure + a combine rule that ignores order, duplication, and timing. What did this post add? — + per-element identity, and the tombstones that identity forces on you. The combine rule is what makes merging safe; stable per-character IDs are what make that rule achievable for a sequence; and not being able to promptly forget a deleted character is the bill for both. A CRDT isn’t magic, and it isn’t just “eventual consistency for documents” — it’s a deliberate restriction of what the data structure is allowed to be, paid for in metadata, in exchange for writing to any replica at any time without asking anyone.

Check yourself

Before you go — a teammate proposes using an OR-Set CRDT for a bank’s “pending transfers” list, reasoning that a set with automatic merge means they’ll never lose a transfer. What would you tell them?

Answer

They’re right that nothing gets lost, and that’s the problem. A CRDT guarantees every replica reaches the same state, not the intended one. Two branches concurrently approving transfers against a balance of $1,000 will merge into a set containing both — deterministically, identically on every replica, and overdrawn. Convergence is a statement about replicas agreeing with each other, never about the result satisfying an invariant that spans them. Any constraint of the form “the sum must stay under X” needs coordination, because checking it requires knowing what the other replica did before you act. Losing a character in a doc is annoying; the CRDT’s answer here is wrong in a way no amount of merging fixes.

And one more — if tombstones are the cost of deletion, why can’t a client just drop tombstones older than, say, a week?

Answer

Because “old” is measured in wall-clock time and the hazard is measured in what other replicas have seen. A laptop that’s been closed for a month can wake up holding an operation anchored to a character you garbage-collected, and there’s now no way to place it. Safe collection needs the stronger condition “every replica has seen every operation up to point T,” which requires hearing from every replica — a coordination round. That’s why tombstone GC is the part of CRDT engineering that stays hard: the whole design was built to avoid needing everyone’s agreement, and cleanup is the one operation that needs it.

Going deeper