How Notion syncs edits across devices in real time
How Notion uses operational transforms, block-level syncing, and optimistic local updates to keep documents consistent across multiple editors.
The scenario
Two people edit the same page while their clients have different network latency; a third person may keep editing offline. The product must feel immediate locally, preserve edits, and converge to one page state when operations meet again.
A useful mental model is a block tree with small operations rather than a single opaque document blob. The server provides an ordering point for each collaborative scope, while clients optimistically apply local edits and reconcile anything they missed.
30-second mental model
A client applies an edit locally, sends an operation over a persistent connection, and records the server version it has seen. The sync service sequences operations and transforms or rebases concurrent operations that touch the same block. Offline operations remain durable on the client and are replayed against the newer server history after reconnect.
This article describes a realistic Notion-like architecture from public product behavior and common collaboration techniques; Notionβs private implementation details and exact infrastructure are not assumed.
5-minute end-to-end flow
- Represent a page as a tree of blocks so unrelated blocks can synchronize independently.
- Apply a local insert, delete, or property update immediately and append it to an outbox.
- Send the operation with the block ID, base version, operation ID, and the clientβs session metadata.
- The server validates permissions, sequences the operation for that block, transforms it against intervening edits, and appends durable history.
- Broadcast the accepted operation to connected collaborators; clients acknowledge or deduplicate by operation ID.
- On reconnect, exchange the last-known version, download missing operations, rebase pending local work, and surface an explicit conflict only when automatic convergence is unsafe.
The Architecture
Before we look at the diagram, here is the mental model. Think of a Notion page as a tree of Lego blocks. Each block has an ID, a type (paragraph, heading, image, toggle, database row), content, and a parent pointer. The entire page is a tree rooted at the page block. This tree is what gets synced.
Every edit is an "operation" on a specific block: insert characters at position 5, delete characters from position 3 to 7, change the block type from paragraph to heading, move the block under a different parent. Operations are small, serializable, and composable.
Here is how the pieces fit together. When User A types "hello" into block B42, the client applies the change locally (you see it instantly) and sends the operation over the WebSocket. The operation includes the block ID, the position, the content, and the client's version vector.
The WebSocket gateway routes the operation to the sync engine, which handles that page. The OT transform engine checks if any other operations arrived for the same block since User A's last known version. If User B also edited block B42, the engine transforms both operations so they produce the same final state regardless of arrival order.
The transformed operation gets persisted to the operation log (append-only, durable) and the block store gets updated. Then the transformed operation is broadcast to all other clients in the page room. Each client applies the transformed operation to their local state.
The key insight: the server is the single source of truth for operation ordering. Clients are optimistic, but the server resolves conflicts.
The important detail is the operation log separately. Every operation (insert, delete, format change, block move) is appended to an immutable log. This log serves three purposes: durability (if the block store crashes, you can replay the log), version history (Notion's "page history" feature reads from this log), and debugging (when something goes wrong, you can trace exactly what happened).
The snapshot store is an optimization. Instead of replaying the entire operation log to load a page, the system periodically snapshots the full page state. A page load fetches the latest snapshot plus any operations since the snapshot.
Notion stores pages as trees of blocks, not as flat documents. A page might have 200 blocks. When two users edit different blocks, there is zero conflict. The hard case (same block, overlapping edits) is actually rare in practice.
Block-Level OT and Conflict Resolution
This is where the design review gets interesting. The reader wants to know: when two users type into the same paragraph at the same time, what actually happens?
Let me start with the intuition before the diagram. Imagine two people writing on the same whiteboard. Person A writes "brown" in the middle of a sentence. Person B, who did not see A's edit yet, writes "lazy" at the end. When you look at the whiteboard, you want both words to be there, in the right positions. That is what OT does: it adjusts positions so both edits land correctly.
Here is the intuition. User A inserts "brown " at position 10. User B inserts "lazy " at position 14. Both operations are based on version 5 of the block.
The server processes operations in arrival order. A arrives first, so it applies directly. Now the server state is at version 6. When B arrives (also based on version 5), the server sees that A already modified the text. Since A inserted 6 characters before B's insertion point, B's position needs to shift right by 6. Position 14 becomes position 20.
This is operational transform in its simplest form: adjust positions based on what happened between the client's version and the server's current version.
The beauty of this approach is that it generalizes. No matter how many concurrent editors there are, each new operation just needs to be transformed against the operations it missed. The transform function is associative: transforming against ops A then B gives the same result as transforming against the compound of A and B.
The key point: you do not need to memorize the transform lookup table. Just explain the intuition: "if someone inserted characters before my cursor position, my position shifts right by the number of characters they inserted." That sentence communicates the core idea.
A common design review mistake is saying "just use CRDTs" without understanding the tradeoff. CRDTs guarantee convergence without a central server, but they produce larger metadata overhead (each character might carry a unique ID and vector clock). Notion chose server-based OT because they already have a central sync server, and OT is simpler for their block-level model.
Offline Editing and Sync Reconciliation
The second hard problem: what happens when your laptop loses WiFi while you are editing, and you keep typing for 10 minutes?
When the client goes offline, it keeps working. Every edit is applied to the local block tree and appended to a pending operations queue stored in IndexedDB (so it survives browser crashes). The UI shows a subtle "syncing" indicator, but the editing experience is unchanged.
This is the magic of optimistic local-first design. The user has no idea they are offline (beyond the indicator). They can type, reorder blocks, add images, even create new sub-pages. Everything works because the client has a complete copy of the page's block tree in memory.
On reconnect, three things happen in order:
- Catch-up: The client tells the server "I last saw version 42." The server sends all operations from v43 to v58 (everything that happened while offline). The server can serve this efficiently because operations are stored in an append-only log indexed by version number.
- Rebase: The client takes its 15 pending operations and transforms each one against the 16 server operations. This is like a git rebase: replay your changes on top of the new base.
- Apply: The client sends the transformed operations to the server. The server validates, persists, and broadcasts them to other clients.
The rebase step is the critical one. Each pending operation must be transformed against each server operation in sequence. For 15 pending ops and 16 server ops, that is 240 transform calculations. With simple text operations (insert, delete), each transform is O(1), so the whole rebase completes in under a millisecond.
A concrete example: you typed "Meeting notes:" as the first line of a new block while offline. Meanwhile, someone else added a heading block above your block. The heading insertion does not affect your text operation at all (different block ID). But if someone else also edited the same block, your "insert 'Meeting' at position 0" might become "insert 'Meeting' at position 15" after transform. The server handles the math; your text lands in the right place.
The key point: the phrase "operation-based rebase, like a git rebase for text operations" communicates the concept instantly.
Notion stores pending operations in IndexedDB, which is a browser-native database that persists across page reloads and crashes. This means you can close your laptop lid, reopen it hours later, and your offline edits are still there. This is a critical UX detail that distinguishes production-grade local-first apps from demos.
The reconciliation process also handles a subtle case: what if User A was editing block B42 offline, and User B deleted block B42 while A was offline? When A reconnects, the rebase discovers that B42 no longer exists. The system handles this by silently dropping A's edits to that block. This is a data loss scenario, but it is the correct behavior: the block was deliberately deleted by another user, and edits to a deleted block have no meaningful target.
Some systems allow "undeleting" the block and applying the edits, but this introduces ghost content that the deleter did not intend to keep. Notion's approach is conservative: deleted blocks stay deleted.
WebSocket Connection Management at Scale
The third challenge is keeping millions of WebSocket connections alive and routing operations efficiently. This is the infrastructure problem beneath the collaboration problem.
The connection tier handles millions of persistent WebSocket connections across a fleet of stateless WS servers. Each server holds ~50K connections and tracks which "page rooms" its connected clients belong to.
When a user opens a Notion page, the client establishes a WebSocket connection and joins the room for that page. The load balancer uses sticky sessions so reconnections go to the same server (preserving the room subscription).
The tricky part is cross-server broadcast. If User A is connected to WS Server 1 and User B is on WS Server 2, but both are editing the same page, operations must cross server boundaries. Redis Pub/Sub handles this: each page has a channel, and WS servers subscribe to channels for pages they have active clients on.
The numbers here are important for your design review answer. A single WS server can handle 50,000-100,000 concurrent WebSocket connections on commodity hardware (the limiting factor is memory for connection state, not CPU). With 200 WS servers, you support 10-20 million concurrent users. Redis Pub/Sub can handle millions of messages per second with sub-millisecond latency, so the cross-server hop adds less than 1ms to the delivery path.
Connection health is another critical detail. The WS server sends heartbeat pings to every client every 30 seconds. If a client does not respond to 3 consecutive pings, the server closes the connection and removes the client from its page rooms. This prevents "ghost connections" from consuming resources.
The key insight for your design review: "Separate the connection layer from the sync logic. WebSocket servers are stateless and horizontally scalable. Sync engines are sharded by page. Redis Pub/Sub bridges the two layers." This one sentence shows you understand the core infrastructure pattern.
How Block-Level Sync Compares to Document-Level Sync
This is a question design reviewers love to ask as a follow-up: "How is this different from Google Docs?" Here is a quick comparison that shows why you need to understand both approaches.
In Google Docs, inserting a character at position 50 shifts every subsequent character position in the entire document. In Notion, inserting a character at position 50 in block B42 affects nothing outside block B42. This means Notion's OT engine does far fewer transforms: most concurrent edits target different blocks and require zero transformation.
The tradeoff is that Notion's data model is more complex (a tree of typed blocks vs a flat character stream), and operations like "move block from page A to page B" do not exist in Google Docs at all. But for collaborative editing with many concurrent users, the block model scales much more naturally.
Bottlenecks, failure modes, and operations
-
Cursor presence and awareness: Showing where other users' cursors are requires a separate, higher-frequency channel. Cursor positions change on every keystroke, but you do not want to run full OT on cursor movements. Most systems send cursor positions as ephemeral messages (not persisted, not transformed) at 10-15 Hz. The cursor position includes the block ID and character offset, so it renders correctly even when other users are editing the same block. When the cursor owner's operations are transformed, their cursor position updates to match.
-
Block reordering conflicts: OT for text within a block is well-understood. But what happens when User A drags block 5 above block 3, and simultaneously User B deletes block 3? The tree structure of Notion's block model makes parent-child moves especially tricky, because a move can create a cycle (block A inside block B inside block A). The OT engine must detect and reject cyclic moves, which means tree-level operations need dedicated transform logic beyond the text insert/delete transforms.
-
Large paste operations: A user pastes 200 lines of text from a Google Doc. That is a single operation from the user's perspective, but the system needs to create 50+ new blocks. If another user is editing during the paste, the OT engine must transform against a compound operation. The typical approach is to decompose the paste into atomic block-creation operations, each of which is individually transformable.
-
Undo across collaborative edits: When you press Cmd+Z, should it undo YOUR last edit, or the last edit to the document (which might be someone else's)? Notion undoes your own edits, which means maintaining a per-user undo stack that correctly accounts for transforms from other users' operations. If you undo an insert at position 5, but someone else inserted text at position 3 since then, your undo (delete at position 5) must be transformed to delete at position 6.
-
Version history and snapshots: Notion's page history shows snapshots every few hours. Reconstructing a snapshot from the operation log requires replaying potentially millions of operations. The solution is periodic snapshot checkpointing: snapshot the full page state every N operations, so history reconstruction only replays from the nearest checkpoint.
A subtle trap: engineers sometimes say "just use database transactions for consistency." But Notion is not doing traditional CRUD on a relational schema. The block store is an eventually consistent tree that converges through OT, not through ACID transactions. SQL transactions solve a different problem (isolation between unrelated queries), not convergence of concurrent collaborative edits.
Common mistakes and misconceptions
| Mistake | What they say | Why it is wrong | What to say instead |
|---|---|---|---|
| Conflating OT and CRDT | "Notion uses CRDTs for real-time sync" | Notion uses server-based OT, not CRDTs. CRDTs have no central server. OT with a central server is simpler for Notion's model. | "They use OT with a central sequencing server. CRDTs would work for peer-to-peer, but Notion already has a server." |
| Ignoring the data model | "Two users editing the same document creates conflicts" | Notion's block-level model means two users editing different blocks have zero conflicts. The conflict surface is much smaller than a flat document. | "Conflicts only happen within a single block. Different blocks = zero coordination needed." |
| Thinking sync is HTTP | "The client polls for changes every second" | Polling at 1-second intervals means 1-second edit latency. Real collaboration needs sub-100ms delivery. WebSockets are mandatory. | "A persistent WebSocket connection delivers operations in under 100ms." |
| Forgetting offline | "Just reject edits when offline" | Notion positions itself as a productivity tool. Losing edits on an airplane would be a deal-breaker. | "All edits are stored locally in IndexedDB and rebased on reconnect." |
| Oversimplifying OT | "Just merge the text like git" | Git's three-way merge works for files with discrete lines. Text within a paragraph does not have natural merge boundaries. You need character-level transform logic. | "OT transforms character positions, not line-level merges. Insert at pos 5 gets adjusted if someone else inserted at pos 3." |
Practical checklist
- Make the block/document data model explicit before choosing OT, CRDTs, or another merge strategy.
- Include operation IDs, base versions, authorization context, and enough metadata to detect duplicates and stale edits.
- Apply local edits optimistically, but persist the client outbox durably enough to survive process restarts.
- Bound transformation/rebase work and define a user-visible path for edits that cannot be merged automatically.
- Partition synchronization by page or block while preserving a clear serialization point for conflicting operations.
- Reconnect with a cursor, replay missing history, and make acknowledgements idempotent.
- Measure connection churn, operation latency, outbox age, conflict/rebase failures, and divergence repairs.
- Avoid presenting a realistic reference architecture as a claim about a private product implementation.
Test Your Understanding
Quick Recap
- Notion models everything as blocks in a tree, and this block-level granularity is what makes real-time collaboration tractable, because changes to different blocks are completely independent.
- Edits apply to your local state immediately (optimistic updates) and sync to the server over a persistent WebSocket connection, giving the user zero-latency typing.
- The server uses operational transform to resolve concurrent edits to the same block by adjusting character positions based on what changed since the client's last version.
- Offline edits queue in IndexedDB and get rebased against server operations on reconnect, preserving every character of the user's work.
- The infrastructure separates stateless WebSocket servers (for connections) from sharded sync engines (for OT), bridged by Redis Pub/Sub for cross-server broadcast.
- Cursor presence is sent as ephemeral high-frequency messages, separate from the durable OT pipeline, at 10-15 Hz.
- Version history uses periodic snapshot checkpointing so reconstruction does not replay the entire operation log from the beginning of time.
- OT with a central server is simpler than CRDTs for Notion's block model, because blocks have straightforward edit semantics and the server is already there for permissions, search, and storage.
Related Concepts
- Operational Transform (OT): The algorithm family Notion uses for conflict resolution. Understanding OT's transform functions (how insert-before-insert and delete-before-insert work) is essential for any collaborative editing design review question.
- CRDTs (Conflict-free Replicated Data Types): The alternative to OT used by Figma and some local-first apps. CRDTs guarantee convergence without a central server but carry more per-operation metadata. Know when to pick each.
- WebSocket connection management: The infrastructure pattern of maintaining millions of persistent connections with sticky load balancing and Redis Pub/Sub bridging, which applies to any real-time system (chat, gaming, live dashboards, collaborative tools).
- Event sourcing and operation logs: Notion's append-only operation log is an event sourcing pattern. The operation log IS the source of truth, and the block store is a materialized view derived from it. Understanding this helps with any question about audit trails, version history, or state reconstruction.
- Optimistic concurrency control: The broader pattern of applying changes locally before server confirmation, used in databases (optimistic locking), UI frameworks (optimistic UI updates), and collaborative tools (local-first editing).