What you'll learn
Quick Answer
A CRDT (Conflict-free Replicated Data Type) is a data structure designed so that any two replicas of it can be merged by a fixed mathematical rule and always land on the exact same result, no matter what order the merges happen in or how many times they're repeated. That property is what lets multiple devices edit the same data offline and reconcile automatically later, with no central server resolving conflicts.
The Problem CRDTs Solve
Imagine a shopping cart that syncs across your phone and laptop. You add an item on your phone while it's offline on the metro. Your laptop, at the same time, is also offline and you remove a different item. Later, both devices come back online and need to merge their changes. There's no server in the loop at the moment each edit happens — so who decides the final state, and how do the two devices even agree on what that state is, without one of them just overwriting the other's work?
The traditional answer is to route every write through a single source of truth — a server that accepts writes one at a time and resolves order. That works, but it means offline edits aren't really possible (you have to wait for connectivity to even attempt the write), and it puts a coordination bottleneck in the middle of every single change.
CRDTs take a different approach entirely: design the data structure itself so that merging two independently-edited copies is mathematically guaranteed to produce a consistent, agreed-upon result, regardless of the order the merges happen in, how many times a merge is repeated, or which device initiates it. No coordinator, no locking, no 'last write wins and hope that's fine.' This is why real-time collaborative tools lean on them — collaborative tools such as Google Docs, Apple Notes, and the design tool Figma are described as using CRDTs so that changes made by one user are seen by all, and Apple specifically implements CRDTs in the Notes app for syncing offline edits between multiple devices.
The mathematical property that makes this work is that CRDT merge operations have to be commutative (order doesn't matter), associative (grouping doesn't matter), and idempotent (merging the same thing twice is harmless) — the same algebraic properties as a simple operation like 'take the maximum of two numbers,' which is exactly the building block the first example below uses.
The Simplest CRDT: a Grow-Only Counter (G-Counter)
The easiest CRDT to understand is a counter that only ever goes up — think 'likes on a post.' The trick is that no replica directly tracks a single shared number. Instead, each replica (server, or device) tracks its OWN contribution in a separate slot, and the total is just the sum of all slots. Merging two replicas means taking the maximum value for each slot, not adding anything:
class GCounter:
def __init__(self, node_id):
self.node_id = node_id
self.counts = {} # node_id -> count; a node only increments its OWN slot
def increment(self, n=1):
self.counts[self.node_id] = self.counts.get(self.node_id, 0) + n
def value(self):
return sum(self.counts.values())
def merge(self, other):
merged = dict(self.counts)
for k, v in other.counts.items():
merged[k] = max(merged.get(k, 0), v) # merge = max per-node slot
self.counts = mergedHere's that running for real: three servers (A, B, C) each receive some 'like' clicks independently, with no coordination between them, and then get merged in a deliberately random order (simulating messages arriving out of sequence over a gossip network) several times over:
=== G-Counter CRDT: likes on a post, replicated across 3 servers ===
Before merge -- A sees 3, B sees 5, C sees 2 (each only knows its own writes)
After random gossip merges -- A=10, B=10, C=10 (expected total = 3+5+2 = 10)
All three replicas converged to the SAME value (10) despite merging in random order.All three servers converged on exactly 10 — the true total (3+5+2) — no matter that the merges happened in random order and some pairs were merged more than once. The reason max() is the right merge function (instead of, say, adding the counts together) is that each node's slot is monotonically non-decreasing — a replica's own slot value only ever grows — so taking the max is always safe: it can never undercount a slot that has already been seen, and re-merging the same information twice doesn't double-count it, because max(x, x) = x. That's idempotency working in practice, not just in a definition.
A Harder Case: Concurrent Add and Remove (OR-Set)
Counters that only increase are the easy case. The genuinely hard CRDT problem is a set that supports both adding and removing elements, where two replicas might concurrently add and remove the SAME element without knowing about each other's action. A naive 'remove just deletes the element' approach breaks badly here: if replica 1 removes 'X' while replica 2 concurrently re-adds 'X', which one wins after merge?
The OR-Set (Observed-Remove Set) solves this by tagging every add with a unique identifier, and having 'remove' only tombstone the specific tags it has actually observed — not the element name in general. A later re-add gets a brand new tag that no remove operation has ever seen, so it survives any merge with an earlier remove:
def remove_all_visible(self, element):
# tombstones only the (element, tag) pairs THIS replica has seen so far
for (e, t) in list(self.adds):
if e == element:
self.removes.add((e, t))
def merge(self, other):
self.adds |= other.adds
self.removes |= other.removes
def elements(self):
return {e for (e, t) in self.adds if (e, t) not in self.removes}Running this for two devices sharing a cart-tags set, where device 1 removes 'laptop-sleeve' while device 2, independently and concurrently, re-adds the exact same item with a fresh tag:
=== OR-Set CRDT: a shared cart-tags set edited concurrently on two devices ===
Device 1 (removed, saw only tag t1): set()
Device 2 (re-added with new tag): {'laptop-sleeve'}
After merging both ways:
Device 1 final set: {'laptop-sleeve'}
Device 2 final set: {'laptop-sleeve'}
Both devices converged to the identical set: True'laptop-sleeve' survives on both devices after merging, because the re-add's tag was never in anyone's tombstone set — only the old tag was marked removed. This is the 'add-wins' semantics that OR-Sets are known for: a concurrent add always beats a remove that didn't observe it, which matches user intuition (if you click 'add to cart' again, you'd be annoyed if a stale, already-in-flight remove from another device silently cancelled it).
State-Based vs Operation-Based, and the Common CRDT Types
CRDTs split into two broad families based on what actually gets sent over the network during replication. State-based CRDTs (CvRDTs) — convergent replicated data types — send the entire current state to other replicas, which merge it in using a merge function (exactly what the G-Counter and OR-Set demos above do). This is simple to reason about but can mean sending a lot of data if the state is large. Operation-based CRDTs (CmRDTs) — commutative replicated data types — instead send just the individual operations (like 'increment' or 'add X') to other replicas, which is more bandwidth-efficient but requires a reliable, at-least-once delivery channel for the operations themselves, since a lost operation message can't be reconstructed from state alone.
Beyond G-Counter and OR-Set, a few other CRDT types cover most practical needs: a PN-Counter extends G-Counter to support decrements too, by tracking separate increment and decrement G-Counters per node and taking the difference. A G-Set is a grow-only set (no removes at all — the simplest possible set CRDT). An LWW-Register (Last-Write-Wins Register) stores a single value tagged with a timestamp, and merge just keeps whichever value has the later timestamp — simple, but it can silently drop a concurrent write if two writes happen to land on the same replica's clock tick, which is a real trade-off compared to the more careful tagging that OR-Set uses.
Text-editing CRDTs (the kind behind collaborative document editors) are considerably more involved than any of these — they need to handle inserting and deleting individual characters in a shared sequence while preserving everyone's intended character order, which is an active area of its own (algorithms like RGA, Logoot, and others exist specifically for this).
A useful way to decide which type you need in practice: start from the operation you actually want to support, not the data structure. If the only operation is 'increase a number' (view counts, like counts), a G-Counter is enough, and it's the simplest possible CRDT to reason about and debug. If you need 'increase and decrease' (available inventory, a score that can go down), reach for a PN-Counter rather than trying to retrofit decrements onto a G-Counter. If you need a collection where items can be added and removed by different replicas independently, an OR-Set's add-wins semantics are usually what people actually want, even if a simpler G-Set or LWW-based set looks tempting at first glance — the concurrent add/remove case this post demonstrated is common enough in real usage that it's worth the extra tagging overhead from the start.
Where CRDTs Actually Run in Production
Beyond the collaborative-editing examples already mentioned, CRDTs show up as native data types inside distributed databases. The NoSQL distributed databases Redis, Riak, and Cosmos DB have CRDT data types built in — meaning you can pick a CRDT-backed counter or set as a first-class data type without implementing the merge logic yourself. The following commercial systems are specifically called out as using CRDTs: Riak, Redis, and Akka.
CRDTs have also been used in online chat systems, online gambling platforms, and the SoundCloud audio distribution platform — generally anywhere multiple replicas (often geographically distributed, for latency reasons) need to accept writes independently and reconcile later without a single coordinating write path.
The throughline across all of these: CRDTs are the right tool specifically when availability and offline-tolerance matter more than perfect, immediate consistency, and when the data shape (counters, sets, registers, sequences) fits one of the known CRDT patterns. They are a deliberate trade, not a free upgrade over regular data structures.
It's worth noticing the pattern in which systems reach for CRDTs versus which ones don't. Collaborative editors and offline-first mobile apps reach for them because the alternative — forcing every edit through a single server before it's considered real — directly conflicts with the product requirement of 'it should feel instant and work offline.' Distributed databases that expose CRDT types (Redis, Riak, Cosmos DB) do it because their customers are building exactly those kinds of multi-region, occasionally-disconnected applications and would otherwise have to hand-roll the same merge logic themselves on top of plain counters and sets. In both cases, CRDTs aren't chosen because they're more powerful than a database transaction — they're chosen because the product genuinely cannot afford to wait for one.
It's also worth noting the appearance of Akka again here, this time as a system that is said to use CRDTs rather than just the actor model — Akka's distributed data module ships CRDT implementations (counters, sets, maps built from CRDT primitives) specifically so that actor-based systems spread across multiple machines can keep small pieces of replicated state consistent without routing every read and write through a single leader actor, which would otherwise undercut the whole point of distributing the system across machines in the first place.
What CRDTs Are Not Good For
CRDTs are not a universal answer to replication, and the honest technical assessment is blunt about it: while CRDTs can deliver a reasonable degree of consistency, it's not perfect, and they are considered unsuitable for high-stakes financial systems, systems of record, and other systems that require strong consistency guarantees.
The core reason is that CRDTs guarantee replicas converge to a consistent state eventually, under a fixed merge rule — but that merge rule has to be decided in advance for the data shape you're using, and it might not match what a human would actually want in every case. A G-Counter can only go up — it is structurally incapable of representing something that needs to go down safely without the PN-Counter trick, and even PN-Counter can't represent something like a bank balance that must never go negative, because CRDT merge rules have no way to enforce cross-field invariants like that during a merge.
There's also a real, practical cost: OR-Sets and similar tombstone-based CRDTs keep remove markers around forever by default (a 'remove' never truly deletes data, it just adds a tombstone), so naive implementations grow unboundedly over time unless you add a separate garbage-collection mechanism for old tombstones once you're confident no replica will ever need them again. That's engineering work layered on top of the core CRDT, not something it solves for free.
There's a further, easy-to-miss cost in the demo above: an OR-Set's storage grows with every add operation ever performed, including ones that were later removed, because the tag has to stick around in the adds set to be correctly matched against a tombstone forever (or until garbage collection runs). A naively long-running cart, chat room, or collaborative document that sees millions of add/remove cycles over months of usage can accumulate a surprising amount of metadata relative to the 'real' current content, which is a genuine operational concern teams running CRDT-backed systems at scale have to actively manage, not an edge case that only shows up in theory.
Frequently Asked Questions
What does CRDT stand for?
Conflict-free Replicated Data Type. It is a data structure replicated across multiple machines or devices, designed so merging any two replicas always produces the same result regardless of merge order, without needing a central coordinator to resolve conflicts.
Are CRDTs the same as 'eventual consistency'?
CRDTs are one specific, mathematically guaranteed way to achieve eventual consistency. Plain eventual consistency just promises replicas will converge 'eventually' without specifying how; CRDTs guarantee it through merge functions that are provably commutative, associative, and idempotent, so convergence isn't just likely, it's mathematically forced.
Why can't you just use Last-Write-Wins for everything?
Last-Write-Wins (keeping whichever write has the latest timestamp) is simple but can silently discard a concurrent write if two writes happen to carry very close or colliding timestamps, which loses data without any error or warning. Structure-aware CRDTs like OR-Set avoid this by tracking enough information (unique tags) to detect and correctly resolve concurrent operations instead of just picking one and discarding the other.
Do CRDTs need a network connection to work?
No, and that is their main selling point. Each replica can keep accepting reads and writes entirely offline, with merging happening whenever connectivity is restored, in whatever order messages happen to arrive. This is exactly why apps like Apple Notes use them for syncing offline edits across devices.
What is the difference between a state-based and operation-based CRDT?
A state-based CRDT (CvRDT) sends its entire current state to other replicas and merges with a merge function. An operation-based CRDT (CmRDT) sends only the individual operations (like a single increment) and relies on a reliable delivery channel to apply them. State-based is simpler to reason about; operation-based uses less bandwidth but needs stronger delivery guarantees.
