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 Problem Statement
Interviewer: "You and a colleague are both editing the same Notion page at the same time. You are on your laptop, they are on their phone. You both type into the same paragraph within a second of each other. How does Notion keep the document consistent without losing either person's changes?"
This question tests four things: whether you understand optimistic local-first updates (apply locally, sync later), how operational transforms or CRDTs resolve concurrent edits, how Notion's block-level data model simplifies the conflict surface, and whether you can reason about the edge cases of offline editing and network partitions.
I think this is one of the hardest situational questions because it sits at the intersection of distributed systems, data modeling, and user experience. The interviewer is not looking for a textbook CRDT definition. They want to see if you understand why Notion chose a block-level model, and how that choice makes real-time collaboration tractable.
Clarifying the Scenario
You: "Good question. Before I jump in, let me clarify a few things."
You: "When you say 'editing the same paragraph,' are we talking about two cursors in the same text block, or two users editing different blocks on the same page?"
Interviewer: "Both. Start with the harder case: same block, overlapping edits."
You: "Got it. And should I assume both users are online, or should I also cover the offline case where one user edits without connectivity and syncs later?"
Interviewer: "Cover the online case first, then I want to hear about offline."
You: "Should I go deep on the specific conflict resolution algorithm, like OT vs CRDT, or stay at the architecture level?"
Interviewer: "Architecture first, then I want you to explain the conflict resolution at an intuitive level."
You: "OK. I will structure my answer in three parts: Notion's block-level data model and why it matters, the WebSocket-based sync protocol that keeps clients in sync, and the conflict resolution strategy for overlapping edits."
My Approach
I break this into five parts:
- Block-level data model: Everything in Notion is a block (paragraphs, headings, images, database rows). This is the foundation that makes real-time sync manageable.
- Optimistic local updates: When you type, the change applies to your local state immediately. You never wait for the server to confirm before seeing your edit.
- WebSocket sync channel: A persistent connection carries operations between client and server in real time, with sub-100ms delivery.
- Operational transform for conflict resolution: When two users edit the same block concurrently, the server transforms one operation against the other so both converge to the same state.
- Offline reconciliation: When a user goes offline, edits accumulate locally. On reconnect, the client replays those edits against the server's current state, transforming as needed.
Let me start with the data model because it determines everything else. If you understand why Notion chose blocks, the rest of the architecture follows naturally.
A Notion page is a tree. The root is the page block. Its children are top-level content blocks (headings, paragraphs, images). Some blocks have their own children (a toggle block contains the blocks inside it, a column block contains the blocks in that column). A database is a block whose children are row blocks, each of which has property blocks.
Each block has a unique ID (UUID), a type, content (varies by type), a parent pointer, and a position among its siblings. The block ID is stable: it never changes even if the block is moved, edited, or duplicated. This stability is what makes OT work, because operations reference blocks by ID, not by position in the tree.
Here is what a simplified block tree looks like for a typical meeting notes page:
| Block ID | Type | Content | Parent |
|---|---|---|---|
| page-1 | page | "Weekly Standup" | (root) |
| block-a | heading | "Discussion" | page-1 |
| block-b | paragraph | "Alice: reviewed PR #123" | block-a |
| block-c | paragraph | "Bob: fixing the deploy" | block-a |
| block-d | heading | "Action Items" | page-1 |
| block-e | to_do | "Deploy fix by Friday" | block-d |
When Alice types into block-b, that operation is scoped entirely to block-b. Bob editing block-c at the same time creates zero conflicts, because they target different blocks with different IDs. This is the fundamental advantage of the block model.
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.
I want to call out 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 interview gets interesting. The interviewer 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.
For your interview: 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 interview 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.
For your interview: 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 interview 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 interview: "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 interviewers 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.
The Tricky Parts
-
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: candidates 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.
What Most People Get Wrong
| 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." |
How I Would Communicate This in an Interview
Here is how I would actually say this. I practice this out loud because the delivery matters as much as the content.
"Notion's real-time sync is built on a block-level data model. Every element on a page, whether it is a paragraph, heading, image, or database row, is a block in a tree. This is the key design choice because it shrinks the conflict surface: two users editing different blocks have zero coordination overhead.
When you type, the edit applies to your local block tree immediately. You never wait for the server. The operation gets sent over a persistent WebSocket connection to the sync server. The server assigns a global sequence number to each operation per block.
If two users edit the same block concurrently, the server uses operational transform to adjust positions. For example, if I insert at position 5 and you insert at position 10, your position becomes 11 because my insertion shifted the text. The transformed operations get broadcast to all clients, so everyone converges to the same state.
For offline editing, the client queues operations in IndexedDB. On reconnect, it sends its last known version, receives all operations it missed, and rebases its pending operations against them, similar to git rebase but for text operations. The result is a seamless merge with no data loss.
The infrastructure layer separates WebSocket connection management (stateless, horizontally scalable) from the OT sync engine (sharded by page). Redis Pub/Sub handles cross-server broadcast when collaborators are on different WebSocket servers."
Notice how I structured this: data model first (the foundation), then the sync flow (the happy path), then conflict resolution (the hard part), then offline (the edge case), then infrastructure (the scale story). This progression shows the interviewer that I think in layers.
I would then pause and ask: "Should I go deeper on any of these? The OT transform logic is the most technically interesting part, and the offline reconciliation is probably the hardest to get right in production."
Interview Cheat Sheet
- When asked about the data model: "Everything is a block. Pages are trees of blocks. Conflicts only happen within a single block, which dramatically simplifies coordination."
- When asked about latency: "Optimistic local updates, zero wait. Operations reach other clients in under 100ms over WebSocket."
- When asked about conflict resolution: "Server-based OT. The server assigns a linear order to operations per block and transforms concurrent operations to preserve intent."
- When asked about OT vs CRDT: "OT with a central server is simpler when you already have a server. CRDTs are better for peer-to-peer (like local-first apps without a backend)."
- When asked about offline: "Operations queue in IndexedDB. On reconnect, the client rebases pending ops against server ops, like git rebase for text."
- When asked about scale: "Stateless WebSocket tier for connections, sharded sync engines for OT, Redis Pub/Sub for cross-server broadcast."
- When asked about undo: "Per-user undo stack. Cmd+Z undoes your edits, not other people's. The stack accounts for OT transforms from concurrent edits."
- When asked about consistency: "Eventual consistency at the block level. The server is the source of truth, and all clients converge within one round trip."
- When asked about version history: "Periodic snapshot checkpointing. Replay operations from the nearest checkpoint to reconstruct any point in time."
- When asked about cursor presence: "Ephemeral messages over WebSocket at 10-15 Hz. Not persisted, not transformed, separate from the OT pipeline."
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 interview 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).