Real-Time Sync Is Harder Than It Looks
Behind every 'saved' indicator is a small distributed systems problem. What's actually happening when you type in a browser tab.
At some point you've probably found a file called something like Meeting Notes (Asgeir's conflicted copy 2024-03-15).docx sitting in your Dropbox folder. That file is sync failing loudly. The rest of the time, it fails quietly.
"Just save it" is how people describe the problem. They mean it as a simple operation: write the current state somewhere durable. But for any app that runs in a browser, syncs across devices, or lets you work in multiple tabs, "save" unpacks into a surprisingly tangled set of questions. Which version is authoritative? What happens if two clients both wrote at the same time? What does "saved" even mean when the server hasn't confirmed anything yet?
I've been thinking about this while building Harbor's sync layer, and I want to explain what's actually happening inside apps that feel fast and reliable — because the machinery underneath is not obvious.
The optimistic update illusion
When you type in Notion, or Google Docs, or almost any modern web app, you see your changes instantly. The save indicator might show a little spinner, then "Saved." This feels like it's describing what happened: you typed, it saved.
But the timing is a lie. The app updated the UI before anything was confirmed. That's an optimistic update — the client assumes the server will accept the change and shows you the result immediately. If the server later rejects it (network error, conflict, version mismatch), the app is supposed to roll back and surface the discrepancy. Many apps handle this well. Some don't, and the rollback just silently clobbers whatever you were doing.
The optimistic model is worth the complexity because the alternative — waiting for a network round-trip before showing you your own keystroke — feels broken. On a 100ms connection, even a brief wait is noticeable. So the app pretends the write already happened and quietly reconciles in the background.
When two clients disagree
The harder problem is conflict. Imagine you leave a document open on your laptop, walk away, and edit it on your phone. When you return to the laptop tab, both have local changes. Which wins?
The naive answer is last-write-wins: whichever write arrives at the server last gets applied. This is simple to implement and genuinely dangerous. A user who spent an hour writing can have their entire session overwritten by a stale five-character update from another device whose clock happened to be slightly ahead. It fails silently. There's no conflict file. The work is just gone.
A better model is versioned writes. Every document gets a version number. Every write includes the version it was based on. If you try to write against version 42 but the server is now at version 45, the write is rejected as stale. The client then has to decide: attempt a merge, surface a conflict, or ask the user what to do.
In Harbor, every document write carries a base_version and a content_hash. If the base is stale, the server tries an automatic rebase — apply the edit at the current position, adjusting for what changed in between. If the changed ranges don't overlap, it's safe and goes through. If they do, the server creates a conflict patch the user can review explicitly. Not automatic. Not silent.
The hard version of the problem
For documents where multiple people type simultaneously — the full Google Docs problem — you need something more sophisticated.
Google Docs, built in 2006, uses Operational Transformation. The idea: instead of sending the raw text, the client sends a description of the edit ("insert 'x' at position 47"), and the server transforms that operation against any concurrent operations that arrived first. So if someone else deleted three characters before position 47, your insert gets adjusted to position 44 before it's applied.
OT works, but it's notoriously hard to get right. The early implementations had subtle bugs that only appeared under specific concurrent-edit sequences. When two operations had to be transformed against each other in both orders, some implementations produced different results depending on which arrived first — which rather defeats the purpose. Google fixed these bugs over years of production use, which is one reason they've never migrated off OT: the working implementation is too valuable to replace.
The alternative that's become popular more recently is CRDTs — conflict-free replicated data types. The idea is to design the data structure so that concurrent writes can always be merged, mathematically, without coordination. Any two replicas that have seen the same set of writes will reach the same state, regardless of order. Figma uses CRDTs. So does Apple Notes' sync layer. Martin Kleppmann at Cambridge has been doing foundational work on this since around 2011, including building Automerge — an open-source CRDT library. His 2019 essay with Ink & Switch, "Local-First Software," made the case that user data should be primary on your own device, with sync as infrastructure rather than dependency.
The tradeoff is that CRDTs are elegant in theory but subtle in practice. Deletion is the classic hard case: you can't truly delete a character from a CRDT without leaving a tombstone — a phantom record that marks it as gone. Accumulate enough edits and you accumulate enough tombstones. The structure grows, the merges get slower. It's a solvable problem, but not a free one.
The tabs problem nobody talks about
There's a version of this that most people never consider: what happens when you have the same document open in two browser tabs?
This is still a sync problem. Both tabs share one server-side document, but each maintains its own local editor state. If you type in tab A, tab B needs to learn about it. The clean solution is a WebSocket: the server broadcasts an event after every confirmed write, and all active clients reconcile their local state against the new version.
But "reconcile" is doing work. The client receiving the event has to decide whether its local state still makes sense. If the user was mid-sentence in tab B when the event arrived, the cursor position might now be wrong. Paragraphs might have shifted. The reconciliation has to be invisible — no flash, no jump, no lost cursor. Most of the time it is. The times it isn't are exactly the times users file bug reports.
This is the background hum of every collaborative app: constant, invisible work to make multiple clients agree on one shared reality.
When sync works, it's transparent. The indicator shows "Saved" and you move on. The engineering behind that indicator is substantial — versioned writes, conflict detection, optimistic state management, WebSocket events, reconciliation logic, conflict patches.
That Dropbox conflict file isn't really a sign that something went wrong. It's a sign something went right. The system detected a genuine disagreement and surfaced it rather than silently picking a winner. Most sync systems never show you that file. They just pick, and you never find out whose changes survived.
Asgeir Albretsen is the founder of Harbor.