CRDT vs operational transform for real-time editing #

You are building collaborative editing — a shared document, a whiteboard, a spreadsheet — and the architecture decision comes down to two families of algorithms. Operational transform (OT) powers Google Docs and many classic editors: clients send operations to a central server, which transforms concurrent operations against each other so they apply cleanly. Conflict-free replicated data types (CRDTs), implemented by libraries such as Yjs and Automerge, give every replica a data structure whose updates merge deterministically in any order, with no central transformation step. Both produce convergent documents. They differ in where the complexity lives, what the server must do, how well they work offline, and what they cost in memory — and those differences decide which fits your WebSocket architecture.

Root cause #

The problem both solve is concurrent edits to the same data. Alice inserts “x” at position 5 while Bob deletes the character at position 3. If each simply applies the other’s operation as received, Alice’s insert lands one position off on Bob’s screen, and the replicas diverge. Some mechanism must adjust for concurrency.

OT adjusts the operations. When the server receives Alice’s insert after having applied Bob’s delete, it transforms the insert — position 5 becomes 4 — before applying and forwarding it. Correctness depends on transformation functions for every pair of operation types, which are notoriously hard to get right beyond plain text, and on a central server that establishes one total order of operations.

CRDTs adjust the data model. Instead of positions, every character (or element) gets a unique, immutable identifier, and an insert says “after element X”. Concurrent inserts after the same element are ordered by a deterministic tiebreaker. Because operations reference identities rather than positions, they commute: any replica can apply them in any order and reach the same state. No server-side transformation is needed.

The same conflict, two resolutions With operational transform the server rewrites Alice's insert position from five to four because Bob's delete was applied first; with a CRDT the insert references an element id and is relayed unchanged, merging correctly in any order. The same conflict, two resolutions Alice Server Bob OT: insert x at 5 OT: delete at 3 (applied first) OT: transform insert → pos 4 CRDT: insert x after id A:17 CRDT: relay unchanged CRDT: merges in any order OT puts intelligence in the server; CRDTs put it in the data structure
Transform the operation, or make operations that never need transforming.

Resolution #

Choose by your constraints rather than by reputation. The matrix below summarises the practical differences for a WebSocket-based product; the paragraphs after it explain the ones that usually decide.

OT and CRDT side by side Operational transform needs a server that orders and transforms operations, handles offline editing poorly and cannot run peer-to-peer, but has small metadata and natural central validation; CRDTs need only a relay, support offline and peer-to-peer use and rich data types, at the cost of per-element metadata and harder central validation. OT and CRDT side by side Operational transform CRDT (Yjs, Automerge) Server role orders + transforms relays + stores Offline editing limited, rebase needed native Peer-to-peer no yes Memory / metadata small ids per element Rich data types transform per pair maps, lists, text Central validation natural harder Most new WebSocket products choose a CRDT library; OT remains strong where a central authority must validate every edit
The deciding rows are usually offline support and central validation.

Server role and scaling. With OT, the server is on the critical path of every edit: it must receive operations for a document in order, transform them, and broadcast the results, so each document is effectively pinned to one process. That is manageable — it is how the big hosted editors work — but it makes the server complex and stateful. With a CRDT, the server relays and stores updates. It still benefits from holding a document replica to answer sync requests, as in syncing Yjs documents over WebSockets, but it never has to understand edit semantics, and relaying between instances is safe in any order.

Offline and flaky connections. CRDT updates made offline merge on reconnect with no special handling. OT clients that edit offline accumulate operations against a stale base and must rebase them through every operation they missed, which works for short gaps and becomes fragile for long ones.

Metadata and memory. CRDTs pay for commutativity with identifiers and tombstones: every inserted element carries an id, and deleted elements often leave markers. Modern implementations compress this heavily — Yjs merges runs of consecutive insertions from one client into single items — so typical text documents cost a small multiple of their plain size, but documents with long, churny histories grow until compacted.

Validation and authority. If every edit must be checked against business rules — permissions per section, schema constraints, locked fields — OT’s central server is a natural enforcement point. With CRDTs, clients apply updates locally before any server sees them, so rejecting an edit means reverting it after the fact. Enforce coarse permissions (who may edit this document) on the connection, and design documents so fine-grained rules are rarely needed.

A practical client for either approach sends edits through the same WebSocket as everything else. With a CRDT library the payloads are opaque binary updates; with OT they are operation objects with revision numbers:

// OT client essentials: operations carry the revision they were made against.
interface TextOp { rev: number; ops: Array<{ retain?: number; insert?: string; delete?: number }> }

class OtClient {
private rev = 0; // last server revision applied
private inflight: TextOp | null = null;
private buffer: TextOp['ops'] | null = null;

constructor(private ws: WebSocket, private transform: (a: any, b: any) => [any, any]) {}

localEdit(ops: TextOp['ops']) {
if (this.inflight) { this.buffer = this.buffer ? compose(this.buffer, ops) : ops; return; }
this.inflight = { rev: this.rev, ops };
this.ws.send(JSON.stringify({ type: 'op', ...this.inflight })); // one op in flight at a time
}

onServerOp(op: TextOp, isAck: boolean) {
this.rev = op.rev;
if (isAck) { // our op was accepted at op.rev
this.inflight = null;
if (this.buffer) { const b = this.buffer; this.buffer = null; this.localEdit(b); }
return;
}
// Someone else's op: transform it against our pending work before applying.
if (this.inflight) [this.inflight.ops, op.ops] = this.transform(this.inflight.ops, op.ops);
if (this.buffer) [this.buffer, op.ops] = this.transform(this.buffer, op.ops);
applyToEditor(op.ops);
}
}
declare function compose(a: any, b: any): any;
declare function applyToEditor(ops: any): void;

The “one operation in flight, buffer the rest” discipline is what keeps OT tractable on the client; the server does the other half of the transformation. A CRDT client, by contrast, just sends every local update and applies every remote one.

Illustrative in-memory size of a text document An OT text document stays at its plain size, while a CRDT starts slightly larger, grows with edit history and shrinks back after compaction. Illustrative in-memory size of a text document plain OT text vs a CRDT with identifiers and tombstones OT (text only) CRDT (with history) 0 KB 20 KB 40 KB 60 KB 10 KB 14 KB Fresh 10 KB doc 10 KB After 50k edits 10 KB 18 KB After compaction
CRDT overhead is real but manageable — and it tracks history, so compaction matters.

Edge cases #

Undo. Collaborative undo should revert my last change, not the document’s last change. Both approaches support this, but it must be built in: Yjs provides an UndoManager scoped to local transactions; OT systems invert and transform the user’s own operations.

Rich text and structure. Text with formatting, nested lists and embedded objects multiplies OT’s transformation pairs. CRDT libraries model these as nested shared types, which is one reason editors such as ProseMirror, TipTap and Lexical have mature Yjs bindings.

Non-text data. For forms and records with independent fields, neither is necessary: per-field last-write-wins is simpler and adequate.

Verification #

Whichever you choose, test convergence with randomized concurrent edits: simulate several clients making random edits while messages are delayed and reordered, deliver everything, and assert every replica ends identical. Run thousands of iterations with a seeded random generator so failures are reproducible. CRDT libraries ship extensive fuzzing of their own, but your integration — how updates are relayed, persisted and replayed — still needs this test.

Operational checklist #

FAQ #

Is a CRDT always better than OT? #

No. CRDTs make offline editing, peer-to-peer sync and horizontal scaling simpler, which is why most new products choose them. OT remains a good fit when a central server must validate every edit or when per-document memory must stay minimal.

Does Google Docs use OT or CRDTs? #

Google Docs is built on operational transform with a central server establishing operation order. Many newer collaborative tools use CRDT libraries such as Yjs or Automerge, or hybrid designs.

Can I use a CRDT with a central server? #

Yes, and most production deployments do. The server relays and persists updates and answers sync requests; it simply never needs to transform operations.

What about Automerge versus Yjs? #

Both are mature CRDT libraries. Yjs is optimised for text editing performance and has broad editor bindings; Automerge emphasises a JSON-like document model with rich history. Evaluate them on your data shape and editor.

Back to Collaborative Editing with CRDTs.