How I Solved Concurrent Document Editing: Why WebSockets Alone Couldn't Handle It

Real-time collaboration seems deceptively simple. Open two browser tabs, connect them with WebSockets, broadcast every change, and you've built something like Google Docs... right? That was my initial assumption. It worked perfectly until two users started editing the same document at the exact same time. That's where things became interesting. My application allowed multiple users to edit a shared document. The architecture looked like this: Client A ──┐ ├──── WebSocket Server ─── Client B Clie
Real-time collaboration seems deceptively simple.
Open two browser tabs, connect them with WebSockets, broadcast every change, and you've built something like Google Docs... right?
That was my initial assumption.
It worked perfectly until two users started editing the same document at the exact same time.
That's where things became interesting.
The Problem
My application allowed multiple users to edit a shared document.
The architecture looked like this:
Client A ──┐
├──── WebSocket Server ─── Client B
Client C ──┘
Enter fullscreen mode Exit fullscreen mode
Every edit made by a user was immediately sent to the server, which then broadcasted the update to every connected client.
For a single user, everything worked flawlessly.
The problem appeared when edits happened concurrently.
Imagine the document contains:
Hello World
Enter fullscreen mode Exit fullscreen mode
Now suppose two users edit it simultaneously.
User A
Hello Amazing World
Enter fullscreen mode Exit fullscreen mode
User B
Hello Beautiful World
Enter fullscreen mode Exit fullscreen mode
Both updates are sent over WebSockets within milliseconds of each other.
The server receives both messages.
Now comes the difficult question.
Which one should win?
Why WebSockets Can't Solve This
A common misconception is that WebSockets provide synchronization.
They don't.
WebSockets guarantee that messages are delivered over a persistent connection.
They do not understand:
- document structure
- insertion positions
- conflicting edits
- concurrent operations
- merge strategies
They're simply a transport protocol.
The server only sees something like:
{
"content": "Hello Amazing World"
}
Enter fullscreen mode Exit fullscreen mode
and moments later
{
"content": "Hello Beautiful World"
}
Enter fullscreen mode Exit fullscreen mode
Without additional logic, the server usually applies the last received update.
This is known as the Last Write Wins (LWW) approach.
While simple, it introduces a major issue.
One user's work disappears entirely.
Trying to Solve It Manually
My first thought was to merge updates on the server.
Sounds reasonable.
Until you consider situations like:
- two users typing at the same cursor position
- one user deleting text while another inserts characters
- users working offline and reconnecting later
- network latency causing edits to arrive out of order
Very quickly, conflict resolution becomes one of the hardest distributed systems problems.
Large collaborative platforms invest years solving these edge cases.
Rebuilding that logic wasn't practical.
Enter Yjs
The solution came in the form of Yjs, a CRDT (Conflict-free Replicated Data Type) library.
Instead of synchronizing the entire document, Yjs synchronizes operations.
Rather than sending:
Entire document changed
Enter fullscreen mode Exit fullscreen mode
it sends operations like:
Insert "Amazing"
at position 6
Enter fullscreen mode Exit fullscreen mode
or
Delete characters
from position 10
to 15
Enter fullscreen mode Exit fullscreen mode
Each operation carries metadata that uniquely identifies it.
Because of this metadata, every client can independently apply operations and still converge to the exact same document state.
No central authority is required to decide which change wins.
How CRDTs Actually Work
A CRDT guarantees three important properties.
1. Every operation has a unique identity
Each inserted character receives an identifier.
Instead of storing:
Hello
Enter fullscreen mode Exit fullscreen mode
Yjs internally represents something closer to:
(H1)(e2)(l3)(l4)(o5)
Enter fullscreen mode Exit fullscreen mode
These identifiers never change.
Even if another user inserts characters between them.
2. Operations are commutative
Suppose two users type simultaneously.
User A inserts:
Amazing
Enter fullscreen mode Exit fullscreen mode
User B inserts:
Beautiful
Enter fullscreen mode Exit fullscreen mode
Even if operations arrive in different orders on different machines, every client eventually computes the same final document.
Arrival order no longer matters.
3. Eventual consistency
Each client maintains its own local copy of the document.
Edits are applied immediately.
Synchronization happens asynchronously.
Once every operation has propagated through the network, every client converges to an identical state.
No manual merge step is required.
Where WebSockets Still Fit
Interestingly, WebSockets never disappeared.
They simply moved to a different responsibility.
Editor
│
▼
Yjs Document
│
▼
WebSocket Provider
│
▼
Other Clients
Enter fullscreen mode Exit fullscreen mode
WebSockets became nothing more than the communication channel.
Yjs became responsible for synchronization, conflict resolution, and consistency.
That's a subtle but extremely important distinction.
What Changed
After integrating Yjs:
- simultaneous edits merged correctly
- no lost updates
- no race-condition bugs
- no manual merge logic
- consistent document state across all connected users
The editor finally behaved the way users expect collaborative software to behave.
Final Thoughts
One lesson stood out from this project.
WebSockets solve communication. CRDTs solve collaboration.
These are fundamentally different problems.
If your application involves multiple users editing the same piece of data, transporting updates quickly isn't enough.
You also need a deterministic way to merge concurrent changes.
For my project, Yjs handled that complexity elegantly, allowing me to focus on building features instead of reinventing distributed synchronization algorithms.
Sometimes the hardest part of real-time software isn't moving data.
It's making sure everyone ends up seeing the same truth.

