Scaling Real-Time Collaboration: Why CRDTs and WebSockets are the 2026 Standard
Stop fighting race conditions with database locks. Learn how to build resilient, local-first collaborative apps using Yjs and WebSockets that handle 50+ concurrent editors without breaking a sweat.

The Fallacy of 'Last Write Wins'
You’re building a collaborative document editor. Two users, Alice and Bob, edit the same sentence at the same time. Alice changes 'The cat sat' to 'The black cat sat.' Bob changes it to 'The cat sat quickly.' In a traditional REST-based architecture, whoever hits the 'Save' button last wins, and the other person’s work is silently deleted. This is a failure of UX and engineering.
For years, we tried to solve this with Operational Transformation (OT), the tech behind Google Docs. But OT is notoriously difficult to implement correctly on the server side because it requires a central authority to sequence every single operation. If the server loses order, the documents diverge. In 2026, we have moved past this. Conflict-free Replicated Data Types (CRDTs) allow us to merge changes mathematically, regardless of the order they arrive or whether the user is offline. When paired with a robust WebSocket layer, you get a system that feels instantaneous and never loses a character.
Why CRDTs over OT?
Operational Transformation requires a complex 'rebase' logic for every concurrent operation. If you have 10 users, the server is doing heavy lifting to ensure everyone sees the same thing. CRDTs, specifically implementations like Yjs or Automerge, treat data as a tree of unique identifiers. Every character added has a unique ID and a pointer to the character before it. If two people insert text at the same spot, the CRDT logic uses these IDs to decide which comes first. The result is 'Strong Eventual Consistency.'
In my experience building production editors at scale, Yjs has become the industry standard because of its performance. It can handle thousands of operations per second because it uses a highly optimized binary format. In 2026, with the widespread adoption of WebAssembly (WASM), CRDT merging happens in sub-millisecond timeframes even on mobile devices.
The Stack: Yjs, Hocuspocus, and Fastify
To build this, you need three components: a client-side CRDT library, a WebSocket transport, and a persistence layer. I recommend using Hocuspocus, which is a suite of tools built on top of Yjs that handles the heavy lifting of WebSocket room management and database integration.
Client-Side Implementation
Here is how you initialize a Yjs document and connect it to a WebSocket provider in a modern React/Next.js environment. We use y-prosemirror if you're building a rich text editor, but the core logic applies to any data type.
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
export const useCollaboration = (roomId: string) => {
// 1. Initialize the Yjs Document
const ydoc = new Y.Doc();
// 2. Connect to the WebSocket Provider
// In 2026, we use secure WebSockets (wss) by default
const provider = new WebsocketProvider(
'wss://api.yourdomain.com/collaboration',
roomId,
ydoc
);
// 3. Define shared types (Text, Map, Array)
const yText = ydoc.getText('content');
return { ydoc, provider, yText };
};
Server-Side Persistence with Hocuspocus
The server isn't just a blind relay. It needs to authenticate users, throttle updates, and persist the binary state to your database. Using Hocuspocus (v2.13.0+), we can hook into the lifecycle of the WebSocket connection.
import { Server } from '@hocuspocus/server';
import { Database } from './db';
const server = Server.configure({\
port: 1234,
async onAuthenticate(data) {
const { token } = data;
const user = await Database.verifyToken(token);
if (!user) throw new Error('Unauthorized');
return { user };
},
async onStoreDocument(data) {
// Yjs documents are stored as Uint8Array blobs
const { documentName, state } = data;
await Database.documents.update(
{ id: documentName },
{ content: state, updatedAt: new Date() }
);
},
async onLoadDocument(data) {
const doc = await Database.documents.find(data.documentName);
return doc?.content || null;
}
});
server.listen();
Scaling the WebSocket Layer
A single Node.js server can handle about 5,000 to 10,000 concurrent WebSocket connections if you tune the memory correctly. However, in a production environment, you need horizontal scaling. This is where most developers fail. You cannot simply put a Load Balancer in front of WebSockets and call it a day.
If User A is on Server 1 and User B is on Server 2, they won't see each other's changes. You need a backplane. In 2026, we use Redis Pub/Sub or NATS to synchronize state between WebSocket nodes. When Server 1 receives a Yjs update blob, it broadcasts it to the Redis channel for that document ID. Server 2 listens to that channel and pushes the update to its connected clients.
Pro Tip: Always use sticky sessions on your load balancer. While CRDTs handle out-of-order updates, keeping a user on the same server reduces the overhead of re-authenticating and re-syncing the entire document state.
The Gotchas: What the Docs Don't Tell You
-
The 'Binary Blob' Problem: CRDT states are binary. If you try to store them as strings in your database, you will corrupt the data. Use
BYTEAin PostgreSQL orBlobin NoSQL. Also, these blobs grow over time. Periodically 'garbage collect' the Yjs document by folding the history into a single snapshot to keep the sync time low. -
Awareness Overhead: We often use WebSockets to show 'User Typing' indicators or mouse cursors. This is called 'Awareness' in Yjs. If you have 50 users moving their mice simultaneously, you are sending 50 updates per second to 50 people. That is 2,500 messages per second per room. Throttle these updates on the client side to 100ms intervals.
-
The 'Undo' Trap: Implementing Undo/Redo in a collaborative environment is a nightmare. If I undo, do I undo my last change or the document's last change? Yjs provides a
UndoManagerthat tracks local changes only. Use it. Do not try to roll your own undo stack based on database timestamps.
Takeaway
Stop treating real-time features as an afterthought or a 'nice to have.' In 2026, users expect every interface to be collaborative. Your action item for today: Replace your current 'Save' button logic for high-frequency data with a Yjs implementation. Start by integrating y-indexeddb for local persistence; it will make your app work offline instantly, and the WebSocket sync will feel like magic once you flip the switch.