---
# === IDENTITY ===
id: software/system-design/realtime-collaboration/2026
canonical_question: "How do I design a real-time collaboration system (Google Docs clone)?"
aliases:
  - "How to build real-time collaborative editing like Google Docs"
  - "CRDT vs OT for collaborative text editing"
  - "System design for multiplayer document editing"
  - "How does Google Docs handle concurrent edits"
entity_type: software_reference
domain: software > system-design > realtime_collaboration
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-23
confidence: 0.90
version: 1.0
first_published: 2026-02-23

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Yjs 13.x / Automerge 2.0 (2023)"
  next_review: 2026-08-22
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Server must enforce total ordering OR use mathematically proven CRDT — partial implementations cause data loss"
  - "Rich-text CRDTs (Peritext) are significantly harder than plain-text — do not assume plain-text solutions transfer"
  - "WebSocket reconnection logic is mandatory — network partitions WILL occur in production"
  - "Undo/redo in collaborative systems cannot use simple stack-based approach — requires intention-preserving undo"
  - "Document size grows unbounded without garbage collection — implement tombstone compaction"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Building a chat application, not collaborative document editing"
    use_instead: "WebSocket pub/sub pattern — no conflict resolution needed for append-only logs"
  - condition: "Only need shared cursors/presence without document sync"
    use_instead: "Presence-only solution (Liveblocks Presence, Phoenix Presence, or simple WebSocket broadcast)"

# === AGENT HINTS ===
inputs_needed:
  - key: "concurrency_level"
    question: "How many simultaneous editors per document?"
    type: choice
    options: ["2-10", "10-100", "100-1000", "1000+"]
  - key: "content_type"
    question: "What type of content is being collaboratively edited?"
    type: choice
    options: ["plain_text", "rich_text", "structured_data", "canvas_graphics"]
  - key: "offline_support"
    question: "Do users need to edit while offline and sync later?"
    type: choice
    options: ["yes", "no"]
  - key: "architecture"
    question: "Is a central server acceptable, or must it work peer-to-peer?"
    type: choice
    options: ["central_server", "peer_to_peer", "hybrid"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/system-design/realtime-collaboration/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-23)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/system-design/websocket-architecture/2026"
      label: "WebSocket Architecture Patterns"
    - id: "software/system-design/event-driven-systems/2026"
      label: "Event-Driven System Design"
  solves:
    - id: "software/debugging/websocket-reconnection/2026"
      label: "WebSocket Reconnection Debugging"
  alternative_to:
    - id: "software/system-design/polling-vs-streaming/2026"
      label: "Polling vs Streaming Architecture"
  often_confused_with:
    - id: "software/system-design/event-sourcing/2026"
      label: "Event Sourcing (similar append-only log, different problem)"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "How Figma's multiplayer technology works"
    author: Evan Wallace (Figma)
    url: https://www.figma.com/blog/how-figmas-multiplayer-technology-works/
    type: technical_blog
    published: 2019-10-01
    reliability: high
  - id: src2
    title: "CRDTs: The Hard Parts"
    author: Martin Kleppmann
    url: https://martin.kleppmann.com/2020/07/06/crdt-hard-parts-hydra.html
    type: academic_paper
    published: 2020-07-06
    reliability: authoritative
  - id: src3
    title: "I was wrong. CRDTs are the future"
    author: Joseph Gentle (ex-Google Wave engineer)
    url: https://josephg.com/blog/crdts-are-the-future/
    type: technical_blog
    published: 2020-04-01
    reliability: high
  - id: src4
    title: "Yjs Documentation — Shared Data Types for Collaborative Software"
    author: Kevin Jahns
    url: https://docs.yjs.dev/
    type: official_docs
    published: 2024-01-15
    reliability: high
  - id: src5
    title: "Understanding sync engines: How Figma, Linear, and Google Docs work"
    author: Liveblocks
    url: https://liveblocks.io/blog/understanding-sync-engines-how-figma-linear-and-google-docs-work
    type: technical_blog
    published: 2024-06-10
    reliability: moderate_high
  - id: src6
    title: "Google Wave Operational Transformation"
    author: Apache Wave (Google)
    url: https://svn.apache.org/repos/asf/incubator/wave/whitepapers/operational-transform/operational-transform.html
    type: official_docs
    published: 2010-01-01
    reliability: authoritative
  - id: src7
    title: "Peritext: A CRDT for Collaborative Rich Text Editing"
    author: Geoffrey Litt, Sarah Lim, Martin Kleppmann, Peter van Hardenberg
    url: https://www.inkandswitch.com/peritext/
    type: academic_paper
    published: 2022-11-01
    reliability: authoritative
---

# Real-Time Collaboration System Design (Google Docs Clone)

## TL;DR

- **Bottom line**: Use a CRDT library (Yjs or Automerge) for new projects; OT only if you need a proven centralized model with simpler server logic.
- **Key tool/command**: `new Y.Doc()` with `y-websocket` provider — production-ready collaborative editing in under 50 lines.
- **Watch out for**: Rich-text formatting merge conflicts — plain-text CRDTs do not handle overlapping bold/italic ranges correctly (use Peritext-style approach).
- **Works with**: Any WebSocket-capable backend (Node.js, Go, Rust, Elixir); Yjs supports 20+ editor bindings (ProseMirror, TipTap, Monaco, CodeMirror, Quill).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Server must enforce total ordering OR use a mathematically proven CRDT — partial implementations cause data loss under concurrent edits
- Rich-text CRDTs (Peritext) are significantly harder than plain-text — never assume plain-text CRDT solutions transfer directly to rich text
- WebSocket reconnection with exponential backoff is mandatory — network partitions WILL happen in production
- Undo/redo requires intention-preserving algorithms, not simple stack-based undo — concurrent edits invalidate naive undo stacks
- Document state grows unbounded without garbage collection — implement tombstone compaction or periodic snapshots

## Quick Reference

| Component | Role | Technology Options | Scaling Strategy |
|---|---|---|---|
| Conflict Resolution Engine | Merges concurrent edits without data loss | Yjs (CRDT), Automerge (CRDT), ShareDB (OT), Google OT | Stateless — runs on each client + server |
| WebSocket Gateway | Persistent bidirectional connection for real-time sync | y-websocket, Socket.IO, ws (Node), Gorilla (Go) | Horizontal scale with sticky sessions per document |
| Document Router | Routes clients to correct server for their document | Consistent hashing on document ID | Hash ring with virtual nodes |
| Presence Service | Tracks who is online, cursor positions, selections | Liveblocks Presence, Phoenix Presence, custom broadcast | Ephemeral state — no persistence needed |
| Awareness Protocol | Broadcasts cursor/selection state to peers | Yjs Awareness, custom WebSocket messages | Piggyback on sync connection; throttle to 10-15 fps |
| Operation Log / WAL | Ordered history of all edits for replay and recovery | Append-only log (Kafka, Redis Streams, PostgreSQL) | Partition by document ID |
| Snapshot Store | Periodic full-document snapshots for fast load | S3, PostgreSQL JSONB, Redis | Snapshot every N ops or T seconds |
| Auth + Access Control | Per-document permissions (view/edit/comment) | JWT tokens validated at WebSocket handshake | Stateless token validation at gateway |
| Offline Queue | Buffers edits when disconnected, replays on reconnect | IndexedDB (browser), SQLite (mobile) | Local-first — syncs on reconnection |
| Rich-Text Formatting Layer | Handles bold, italic, links without merge conflicts | Peritext (CRDT), ProseMirror + Yjs, TipTap + Yjs | Integrated with conflict resolution engine |
| Garbage Collector | Compacts tombstones and reclaims memory | Yjs GC, Automerge compaction, custom sweep | Run during low-activity periods |
| CDN / Edge Cache | Serves static assets; caches read-only snapshots | Cloudflare, CloudFront, Fastly | Cache invalidation on document update |

## Decision Tree

```
START
|-- Need offline editing + peer-to-peer sync?
|   |-- YES --> Use CRDT (Yjs or Automerge)
|   +-- NO
|       |-- Need rich-text collaborative editing?
|       |   |-- YES
|       |   |   |-- Want managed solution?
|       |   |   |   |-- YES --> Liveblocks or TipTap Cloud
|       |   |   |   +-- NO --> Yjs + TipTap/ProseMirror + y-websocket
|       |   +-- NO (plain text or structured data)
|       |       |-- Central server acceptable?
|       |       |   |-- YES
|       |       |   |   |-- < 1K concurrent users per doc?
|       |       |   |   |   |-- YES --> OT (ShareDB) or Yjs — both work
|       |       |   |   |   +-- NO --> Yjs (better memory profile at scale)
|       |       |   +-- NO --> Yjs with y-webrtc provider
|       +-- Canvas/graphics editing?
|           |-- YES --> Property-level LWW (Figma model) [src1]
|           +-- NO --> Yjs with appropriate shared type (Y.Map, Y.Array)
```

## Step-by-Step Guide

### 1. Choose conflict resolution strategy

Decide between OT (Operational Transformation) and CRDT (Conflict-free Replicated Data Type) based on your requirements. CRDTs are mathematically guaranteed to converge without a central server, while OT requires server coordination but has a simpler mental model for centralized architectures. [src3]

**Key differences**:
- OT: transforms operations against each other; requires server-side transformation function; used by Google Docs
- CRDT: data structures that merge automatically; no central coordinator needed; used by Figma (inspired), Apple Notes, and most new projects

**Verify**: List your requirements against the decision tree above. If offline support or P2P is needed, CRDT is the only viable option.

### 2. Set up the CRDT document model

Initialize Yjs shared types that mirror your document structure. Yjs provides Y.Text (for collaborative text), Y.Map (for key-value objects), Y.Array (for ordered lists), and Y.XmlFragment (for rich-text DOM trees). [src4]

```javascript
// npm install yjs y-websocket y-indexeddb
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { IndexeddbPersistence } from 'y-indexeddb';

const ydoc = new Y.Doc();
const ytext = ydoc.getText('document-body');
const ymeta = ydoc.getMap('document-meta');

// Set document metadata
ymeta.set('title', 'Untitled Document');
ymeta.set('lastModified', Date.now());
```

**Verify**: `console.log(ydoc.getText('document-body').toString())` --> expected output: `""` (empty string)

### 3. Connect the WebSocket sync provider

Wire up the network transport layer. The y-websocket provider handles connection management, reconnection, and binary sync protocol automatically. [src4]

```javascript
// Connect to WebSocket server
const wsProvider = new WebsocketProvider(
  'wss://your-server.example.com',
  'document-room-id',  // unique per document
  ydoc
);

// Monitor connection status
wsProvider.on('status', ({ status }) => {
  console.log(`Sync status: ${status}`); // 'connecting', 'connected', 'disconnected'
});

// Add offline persistence via IndexedDB
const indexeddbProvider = new IndexeddbPersistence(
  'document-room-id',
  ydoc
);
indexeddbProvider.on('synced', () => {
  console.log('Loaded from local cache');
});
```

**Verify**: Open two browser tabs with the same document room ID. Type in one tab and confirm text appears in the other within 50-100ms.

### 4. Implement awareness (cursors and presence)

Awareness is a separate protocol from document sync — it broadcasts ephemeral state (cursor position, user name, color) to all connected peers. Unlike document state, awareness data is not persisted. [src4]

```javascript
// Set local user awareness state
wsProvider.awareness.setLocalStateField('user', {
  name: 'Alice',
  color: '#30bced',
  cursor: null  // updated by editor binding
});

// Listen for remote awareness changes
wsProvider.awareness.on('change', ({ added, updated, removed }) => {
  const states = wsProvider.awareness.getStates();
  states.forEach((state, clientId) => {
    if (clientId !== ydoc.clientID) {
      console.log(`User ${state.user?.name} is at position ${state.user?.cursor}`);
    }
  });
});
```

**Verify**: Open two tabs, confirm each shows the other user's cursor position and name.

### 5. Bind to a rich-text editor

Connect Yjs to an editor framework. Yjs has bindings for ProseMirror, TipTap, CodeMirror 6, Monaco, Quill, and more. [src4]

```javascript
// Example: TipTap (built on ProseMirror) + Yjs
// npm install @tiptap/core @tiptap/starter-kit @tiptap/extension-collaboration
//             @tiptap/extension-collaboration-cursor
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Collaboration from '@tiptap/extension-collaboration';
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';

const editor = new Editor({
  extensions: [
    StarterKit.configure({ history: false }), // disable built-in undo — Yjs handles it
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({
      provider: wsProvider,
      user: { name: 'Alice', color: '#30bced' },
    }),
  ],
});
```

**Verify**: Type formatted text (bold, italic) in one tab. Confirm formatting appears correctly in the other tab.

### 6. Deploy the WebSocket server

Run the y-websocket server in production. It handles document rooms, binary sync protocol, and awareness broadcasting. [src4]

```bash
# Install and run the y-websocket server
npm install y-websocket
HOST=0.0.0.0 PORT=1234 YPERSISTENCE=./yjs-docs npx y-websocket

# For production: use pm2 or Docker
# The server persists documents to LevelDB by default
```

**Verify**: `curl -s -o /dev/null -w "%{http_code}" http://localhost:1234/health` --> expected: `200` or connection upgrade response

### 7. Add server-side persistence and snapshots

The y-websocket server stores updates in LevelDB by default, but for production you need durable storage with backup capability. [src1]

```javascript
// Custom persistence callback for y-websocket server
// Save full document snapshots to PostgreSQL/S3 periodically
const { LeveldbPersistence } = require('y-leveldb');
const persistence = new LeveldbPersistence('./yjs-storage');

// Periodic snapshot for fast document loading
setInterval(async () => {
  const docNames = await persistence.getAllDocNames();
  for (const docName of docNames) {
    const ydoc = await persistence.getYDoc(docName);
    const snapshot = Y.encodeStateAsUpdate(ydoc);
    await saveSnapshotToS3(docName, Buffer.from(snapshot));
    ydoc.destroy();
  }
}, 5 * 60 * 1000); // every 5 minutes
```

**Verify**: Stop and restart the server. Reopen the document and confirm all content is preserved.

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

### JavaScript: Minimal CRDT Collaboration with Yjs

```javascript
// Input:  Two users editing the same Y.Text concurrently
// Output: Both edits merged without conflict, converged state

import * as Y from 'yjs'; // yjs@13.6.x

// Simulate two clients with separate Y.Doc instances
const doc1 = new Y.Doc();
const doc2 = new Y.Doc();

// Client 1 types "Hello "
doc1.getText('shared').insert(0, 'Hello ');
// Client 2 types "World" concurrently
doc2.getText('shared').insert(0, 'World');

// Sync: exchange state vectors and updates
const sv1 = Y.encodeStateVector(doc1);
const sv2 = Y.encodeStateVector(doc2);
const update1to2 = Y.encodeStateAsUpdate(doc1, sv2);
const update2to1 = Y.encodeStateAsUpdate(doc2, sv1);
Y.applyUpdate(doc1, update2to1);
Y.applyUpdate(doc2, update1to2);

// Both docs now have identical content (order depends on client IDs)
console.log(doc1.getText('shared').toString()); // "Hello World" or "WorldHello "
console.log(doc1.getText('shared').toString() === doc2.getText('shared').toString()); // true
```

### JavaScript: WebSocket Sync Server (Node.js)

```javascript
// Input:  Multiple WebSocket clients connecting per document room
// Output: Real-time sync of Y.Doc state across all connected clients

const http = require('http');
const WebSocket = require('ws');                  // ws@8.x
const Y = require('yjs');                          // yjs@13.6.x
const { setupWSConnection } = require('y-websocket/bin/utils'); // y-websocket@2.x

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('y-websocket server');
});

const wss = new WebSocket.Server({ server });

wss.on('connection', (ws, req) => {
  // Extract room name from URL path: /roomName
  const roomName = req.url.slice(1).split('?')[0];
  setupWSConnection(ws, req, { docName: roomName });
  console.log(`Client joined room: ${roomName}`);
});

server.listen(1234, () => {
  console.log('WebSocket sync server running on ws://localhost:1234');
});
```

### Python: OT-style Server with Operation Logging

> Full script: [python-ot-style-server-with-operation-logging.py](scripts/python-ot-style-server-with-operation-logging.py) (54 lines)

```python
# Input:  Concurrent text operations from multiple clients
# Output: Transformed operations that maintain document consistency
from dataclasses import dataclass
from typing import Literal
import json
# ... (see full script)
```

## Anti-Patterns

### Wrong: Using simple last-write-wins for text content

```javascript
// BAD — overwrites entire document on each keystroke, losing concurrent edits
socket.on('document-update', (newContent) => {
  document.content = newContent; // last write wins = data loss
});
socket.emit('document-update', document.content);
```

### Correct: Using granular CRDT operations

```javascript
// GOOD — merges character-level operations, preserves all concurrent edits
const ydoc = new Y.Doc();
const ytext = ydoc.getText('content');
ydoc.on('update', (update) => {
  socket.emit('yjs-update', update); // send binary diff, not full document
});
socket.on('yjs-update', (update) => {
  Y.applyUpdate(ydoc, new Uint8Array(update)); // merge, not overwrite
});
```

### Wrong: Sending full document state on every edit

```javascript
// BAD — O(document_size) per keystroke, kills performance and bandwidth
editor.on('change', () => {
  const fullDoc = editor.getContent();
  ws.send(JSON.stringify({ type: 'full-sync', data: fullDoc })); // 100KB+ per keystroke
});
```

### Correct: Sending incremental updates only

```javascript
// GOOD — O(edit_size) per keystroke, typically 10-100 bytes
ydoc.on('update', (update, origin) => {
  if (origin !== 'remote') {
    ws.send(update); // binary diff: only the characters that changed
  }
});
```

### Wrong: Using built-in editor undo with collaborative editing

```javascript
// BAD — built-in undo reverts OTHER users' changes, not just yours
const editor = new Editor({
  extensions: [
    StarterKit, // includes History plugin with standard undo
    Collaboration.configure({ document: ydoc }),
  ],
});
// User A types, User B types, User A hits Ctrl+Z -> undoes User B's edit!
```

### Correct: Using Yjs undo manager scoped to local client

```javascript
// GOOD — undo only affects the local user's operations
import { UndoManager } from 'yjs';

const undoManager = new UndoManager(ytext, {
  trackedOrigins: new Set([null]), // only track local changes
});

const editor = new Editor({
  extensions: [
    StarterKit.configure({ history: false }), // DISABLE built-in undo
    Collaboration.configure({ document: ydoc }),
  ],
});

// Ctrl+Z now only undoes current user's edits
undoManager.undo();
```

### Wrong: No reconnection handling

```javascript
// BAD — connection drops silently, user loses all subsequent edits
const ws = new WebSocket('wss://server.example.com/doc/123');
ws.onclose = () => console.log('disconnected'); // logs and does nothing
```

### Correct: Exponential backoff with offline queue

```javascript
// GOOD — queues edits locally and syncs on reconnection
// y-websocket handles this automatically, but for custom implementations:
function connectWithRetry(url, ydoc, attempt = 0) {
  const ws = new WebSocket(url);
  ws.onopen = () => {
    attempt = 0;
    const localState = Y.encodeStateAsUpdate(ydoc);
    ws.send(localState); // sync full state on reconnect
  };
  ws.onclose = () => {
    const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
    setTimeout(() => connectWithRetry(url, ydoc, attempt + 1), delay);
  };
}
```

## Common Pitfalls

- **CRDT memory bloat**: Yjs stores tombstones for deleted characters. A document with 100K edits can consume 10-50x its visible text size in memory. Fix: call `ydoc.gc = true` (enabled by default) and periodically re-encode with `Y.encodeStateAsUpdate(ydoc)` to compact. [src2]
- **Cursor jumps on high latency**: When sync latency exceeds 200ms, cursor positions desync and "jump" after each remote update. Fix: throttle awareness updates to 50-100ms intervals and use relative positions (`Y.createRelativePositionFromTypeIndex`) instead of absolute indices. [src4]
- **Split-brain from missing server authority**: In pure P2P mode, two network partitions can diverge and produce surprising merges when reconnected. Fix: designate one peer as "authority" or accept that merge results may reorder text. [src2]
- **Rich-text formatting conflicts**: Two users applying bold to overlapping ranges can produce incorrect formatting boundaries. Fix: use Peritext-style formatting marks that attach to character positions, not ranges. [src7]
- **WebSocket message ordering**: TCP guarantees order per connection, but if you load-balance across multiple servers, messages from different servers can arrive out of order. Fix: use sticky sessions (route by document ID) or include vector clocks in messages. [src1]
- **Snapshot load time**: Loading a document by replaying all operations from genesis is O(total_ops). A document with 1M operations takes seconds to load. Fix: store periodic snapshots and replay only operations after the last snapshot. [src1]
- **Browser tab duplication**: Each browser tab creates a separate Y.Doc and WebSocket connection. Two tabs for the same document from the same user cause double presence and potential conflicts. Fix: use BroadcastChannel API or SharedWorker to share a single Y.Doc across tabs. [src4]
- **Undo across formatting boundaries**: Undoing a bold operation that was concurrently modified by another user can corrupt the formatting tree. Fix: use Yjs UndoManager with `captureTransaction` to group related formatting operations atomically. [src7]

## Diagnostic Commands

```bash
# Check WebSocket server health
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: test" \
  http://localhost:1234/

# Monitor active WebSocket connections (Linux)
ss -s | grep -i estab

# Count connected clients per document room (y-websocket with logging)
curl -s http://localhost:1234/rooms | jq '.[] | {room: .name, clients: .clients}'

# Check Yjs document size in bytes (Node.js)
node -e "const Y=require('yjs'); const doc=new Y.Doc(); console.log(Y.encodeStateAsUpdate(doc).byteLength)"

# Monitor WebSocket traffic in Chrome DevTools
# Network tab -> WS filter -> click connection -> Messages tab
```

## Version History & Compatibility

| Library | Version | Status | Key Changes | Notes |
|---|---|---|---|---|
| Yjs | 13.6.x | Current (2024) | Improved GC, Sub-documents | Recommended for new projects |
| Yjs | 13.5.x | Stable | Y.XmlFragment improvements | |
| Automerge | 2.x | Current (2023) | Rust core, 10x faster than 1.x | Breaking API changes from 1.x |
| Automerge | 1.x | Legacy | Pure JS, slow on large docs | Migrate to 2.x |
| ShareDB | 4.x | Current | OT with JSON documents | Mature but less active |
| y-websocket | 2.x | Current | Binary protocol, awareness | Drop-in server for Yjs |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Multiple users edit the same document simultaneously | Single-user editing with simple save/load | Standard REST API with optimistic locking |
| Offline editing with later sync is required | Always-online with reliable network | Server-authoritative OT (ShareDB) |
| Rich-text collaborative editing (Google Docs clone) | Chat or messaging (append-only) | WebSocket pub/sub (Socket.IO rooms) |
| Peer-to-peer collaboration without central server | You already have and require a central server | OT with server transformation |
| Canvas/whiteboard with object-level edits (Figma clone) | Simple form data with field-level edits | Property-level last-write-wins (no CRDT needed) |
| Need to support 100+ concurrent editors per document | 2-3 users with low-frequency edits | Mutex lock or turn-based editing |

## Important Caveats

- CRDT performance has improved dramatically (Yjs processes 6M ops/sec in benchmarks) but memory overhead remains: expect 2-5x document size for metadata in Yjs, more for Automerge 1.x [src3]
- Google Docs uses OT, not CRDTs — but Google has a massive engineering team maintaining their custom OT implementation. For most teams, CRDTs (Yjs) are more practical to implement correctly [src6]
- Figma does NOT use "true CRDTs" — they use CRDT-inspired property-level last-write-wins with a central server, which is simpler but does not support offline editing or P2P [src1]
- The "OT vs CRDT" debate is largely settled for new projects: use CRDTs unless you have specific reasons not to. Joseph Gentle (ex-Google Wave) wrote: "I was mass wrong about CRDTs... everything OT can do, CRDTs can do. The reverse is not true." [src3]
- Rich-text CRDT is an unsolved problem at the academic level — Peritext (2022) is the most promising approach but is not yet integrated into mainstream libraries as a drop-in solution [src7]
- End-to-end encryption is possible with CRDTs (server never sees plaintext) but not with OT (server must understand operations to transform them) [src3]

## Related Units

- [WebSocket Architecture Patterns](/software/system-design/websocket-architecture/2026)
- [Event-Driven System Design](/software/system-design/event-driven-systems/2026)
- [Event Sourcing (often confused with collaborative editing)](/software/system-design/event-sourcing/2026)
