CRDTs vs Operational Transform: What I Learned Building Real-Time Collaboration

Sync··12 min read

Real-time collaboration looks simple when it works. Two people open the same document, both type, both see each other's edits, and nobody thinks about distributed systems.

The hard part is that the product promise is stronger than "send updates quickly." Users expect the editor to feel local, recover from bad networks, preserve intent, and never lose work. That means collaborative editing is not only a WebSocket problem. It is a consistency problem wrapped in a user experience.

When I started working with collaborative editors, I understood the headline version: Operational Transform powers systems like Google Docs, and CRDTs power libraries like Yjs and Automerge. The more useful lesson was less binary:

This post is the mental model I wish I had earlier.

What problem collaborative editing actually solves

A single-user editor has one source of truth: the current document in memory. A collaborative editor has many temporary truths.

Each connected client is editing a local copy. The server may have another copy. Offline clients may have older copies. Network packets can arrive late, out of order, duplicated, or not at all. Still, the system should eventually answer a very human question:

After everyone finishes editing and reconnects, what is the document?

That sounds like a backend concern, but it directly affects the interface:

  • typing must stay instant, even before the server acknowledges it;
  • remote changes must appear without jumping the cursor unexpectedly;
  • offline edits should sync later without overwriting someone else;
  • comments, presence, permissions, and document lifecycle state need to agree with the editor;
  • recovery states must be understandable when sync fails.

The technical goal is convergence. The product goal is trust.

Why naive syncing fails

The simplest design is also the most dangerous one:

1
client.onChange((text) => {
2
socket.send({ documentId, text });
3
});
4
5
socket.on('document:update', ({ text }) => {
6
editor.setText(text);
7
});

This works in demos because demos usually have one user, perfect ordering, and no latency. It fails as soon as two users edit concurrently.

Imagine a document that starts as:

1
Hello world

User A inserts beautiful:

1
Hello beautiful world

At the same time, User B deletes world:

1
Hello

If the server stores whichever update arrives last, one user's work disappears. If clients blindly apply full snapshots, the cursor can jump and local edits can be overwritten. If you add timestamps, you now have a clock problem, not a collaboration algorithm.

Naive syncing confuses transport with semantics. It knows that something changed, but not what the user meant.

Operational Transform in one mental model

Operational Transform, usually shortened to OT, represents edits as operations:

1
insert(position: 6, text: "beautiful ")
2
delete(position: 6, length: 5)

The central idea is: when two operations happen concurrently, transform one operation against the other so both can be applied without losing intent.

For example, if User A inserts text before the position where User B deletes text, User B's delete position may need to shift forward.

1
Initial: Hello world
2
3
A: insert(6, "beautiful ")
4
B: delete(6, 5)
5
6
Transform B against A:
7
B becomes delete(16, 5)
8
9
Result: Hello beautiful

The promise is powerful: keep operations small, preserve user intent, and maintain a consistent document by transforming concurrent edits.

The catch is that OT requires a lot of discipline around history and ordering. The system needs to know which operations each client has seen, which operations are concurrent, and how every operation type transforms against every other operation type. Text insertion and deletion are only the beginning. Rich text, lists, embeds, comments, and structured documents make the transform matrix more complicated.

OT can work extremely well. It is not an outdated idea. But implementing it correctly is hard, and many teams do not want to own that complexity.

CRDTs in one mental model

CRDT stands for Conflict-free Replicated Data Type. The idea is not "there are no conflicts." The idea is that the data type is designed so concurrent changes can merge deterministically.

Instead of asking a central server to decide the one true order of every edit before clients can proceed, each replica can accept local changes and later merge with other replicas.

A useful way to think about a CRDT is:

Every write carries enough identity and ordering metadata that replicas can replay and merge changes without asking, "who wins?"

For simple data types, this can be easy. A counter can store increments per replica and merge by taking the maximum count seen for each replica. A set can track adds and removes with unique operation identifiers.

Text is harder because text is ordered. Two users can both insert at the same visible index, but "index 5" is not stable across replicas. A collaborative text CRDT needs durable identities for pieces of content and deterministic rules for ordering concurrent inserts.

1
Initial: H e l l o
2
3
Replica A inserts X after "e"
4
Replica B inserts Y after "e"
5
6
Both inserts are valid.
7
The CRDT uses identifiers, not wall-clock time, to choose a stable order.

That is the shift: lists are not arrays with retries. They are ordered collections of uniquely identified items.

OT vs CRDTs: the practical difference

The simplified comparison looks like this:

1
Operational Transform
2
Local edit -> operation -> transform against concurrent operations -> apply
3
4
CRDT
5
Local edit -> operation with identity/metadata -> merge deterministically -> converge

OT usually leans more on a coordinated operation history. CRDTs usually lean more on the data model and merge semantics.

In product terms:

  • OT can be efficient and precise, but the server/client protocol and transform logic are complex.
  • CRDTs make offline-first and peer-like merging easier, but metadata and persistence become serious concerns.
  • OT asks, "how should this operation change because another operation happened?"
  • CRDTs ask, "how should all replicas represent state so merging is always safe?"

Neither removes complexity. They choose where it lives.

Tradeoff 1: memory is part of the algorithm

CRDTs need metadata. That metadata is what allows replicas to merge concurrent changes safely.

For collaborative text, the system may need to remember identifiers for inserted content, deletion markers, client clocks, and enough structure to preserve ordering. Even when the visible document is small, the internal representation can be larger than the rendered text.

This surprises people because the UI says "a paragraph," but the sync layer sees history, identities, and deleted ranges.

The practical impact:

  • long-lived documents can accumulate update history;
  • high-churn documents can grow faster than expected;
  • memory usage can be very different from plain-text size;
  • mobile clients and low-end devices feel the cost first.

If you are adding CRDTs to a product, measure document size and update volume early. Do not wait until your largest customer has a document that has been edited for six months.

Tradeoff 2: persistence is not just saving JSON

A normal document can often be persisted as one JSON blob. A CRDT document usually needs more care.

You might store incremental updates, snapshots, or both. Incremental updates are useful because they preserve the CRDT's merge behavior and can be appended efficiently. Snapshots are useful because loading a document from thousands of tiny updates can become slow.

A common shape looks like this:

1
Client edit
2
-> CRDT update
3
-> WebSocket provider
4
-> persistence layer appends update
5
-> periodic compaction creates snapshot
6
-> new clients load snapshot + recent updates

The dangerous part is treating compaction as a background cleanup job with no correctness risk. Compaction changes the recovery path. If you compact incorrectly, clients may load a state that looks valid but has lost merge information.

Persistence also interacts with authorization. If a user loses access to a document, can their offline updates still sync later? Should the server reject them? If rejected, how does the editor explain that to the user without making the document feel broken?

The CRDT can merge data. The product still has to decide which data is allowed to exist.

Tradeoff 3: ordering is deterministic, not always intuitive

CRDTs need deterministic ordering for concurrent inserts. Deterministic does not always mean human-obvious.

If two users type at the same location at the same time, the final order may be based on internal identifiers. That is good because every replica converges. It can still produce text that feels strange in edge cases.

This is one reason collaborative editing UX uses more than just the merge algorithm:

  • presence shows who is nearby;
  • selections communicate intent;
  • editor bindings preserve cursor positions;
  • product design discourages chaotic simultaneous editing in the same sentence;
  • comments and suggestions provide safer workflows for high-stakes edits.

The algorithm gives you convergence. The interface gives users confidence.

Tradeoff 4: garbage collection has product consequences

Deletes are tricky in distributed systems.

If a replica deletes a character, another offline replica may not have seen that character yet. The system needs enough information to make the deletion meaningful when replicas reconnect. This is why CRDT implementations often keep tombstones or other deletion metadata.

Garbage collection asks: when is it safe to throw that metadata away?

The answer depends on your sync model:

  • Are all clients expected to reconnect eventually?
  • Can very old offline clients come back?
  • Do you have server-side snapshots that become the new baseline?
  • Can you force clients to discard local state and reload?
  • Do you need audit history or only the latest document state?

These are not only engineering questions. They affect support, compliance, and user trust. A very aggressive cleanup strategy may save storage but make offline recovery weaker. A very conservative strategy may keep recovery strong but increase memory and storage costs.

Tradeoff 5: CRDTs do not solve every invariant

CRDTs are good at eventual convergence. They are not a replacement for transactions, locks, or consensus when the product requires strict global guarantees.

For example, CRDTs are a poor fit when the invariant is:

  • only one person can claim this resource;
  • inventory must never go below zero;
  • usernames must be globally unique;
  • money must move exactly once and in a strict order;
  • multiple records must update atomically as one business action.

You can make replicas converge on the same answer and still violate the business rule along the way.

1
Stock = 1
2
3
Replica A sells one item while offline -> local stock = 0
4
Replica B sells one item while offline -> local stock = 0
5
6
After merge, the system can agree that two sales happened.
7
It cannot pretend the oversell did not happen.

Convergence is not the same as correctness for every domain.

When you should not use CRDTs

I would avoid CRDTs when:

  • the application does not need concurrent offline writes;
  • last-write-wins is acceptable and easy to explain;
  • conflicts are rare and manual review is better for the product;
  • strict ordering is required for correctness;
  • storage and memory budgets are extremely tight;
  • the team cannot operate the persistence and compaction model safely;
  • the data has cross-object invariants that need transactional enforcement.

This is the part that gets missed in hype cycles. A CRDT is not automatically more advanced architecture. Sometimes it is unnecessary architecture.

For a settings page, a simple version field may be enough. For a payment ledger, you probably want transactional ordering. For a collaborative rich-text editor with offline support, a CRDT starts to make much more sense.

The lesson I keep coming back to

The best mental model I have found is this:

1
WebSockets make collaboration feel live.
2
OT and CRDTs make concurrent edits survivable.
3
Product design makes the result understandable.
4
Infrastructure makes it durable.

CRDTs are impressive because they let local-first systems accept writes without coordinating every edit through a central authority. That is a big deal for collaborative editors. It is also a trade.

You get conflict-free merging, offline tolerance, and a strong convergence model. In return, you must care about metadata growth, snapshots, update logs, garbage collection, authorization, and edge-case UX.

That trade can absolutely be worth it. But it is still a trade.

If I were choosing today, my default would be:

  • use Yjs when building collaborative text, whiteboards, or structured documents with offline/realtime expectations;
  • use OT if I had a mature OT stack already or needed a very controlled central operation pipeline;
  • use simpler versioning when the product does not truly need multi-user concurrent editing;
  • use transactions/consensus when correctness depends on strict global invariants.

The goal is not to pick the most distributed-systems-heavy answer. The goal is to preserve user intent with the least complexity your product can honestly support.

Liked this article? Share it with a friend on X, LinkedIn, or email, , or . Have a question or want to talk about realtime editors, local-first systems, or open source? Email me and I'll do my best to get back to you.

Have a wonderful day.

– Vipin

Continue exploring