WebSockets are straightforward to get working in a demo. They become significantly more complex to operate reliably in production. Here is what I learned building the real-time collaboration features in Devpads.
The Reconnection Problem
Browsers close WebSocket connections constantly — when a user locks their phone, when they switch tabs for a while, when their WiFi drops and reconnects. Your client code must handle reconnection gracefully.
A naive reconnect loop will hammer your server. Use exponential backoff:
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.retryDelay = 1000;
this.maxDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onclose = () => {
setTimeout(() => {
this.retryDelay = Math.min(this.retryDelay * 2, this.maxDelay);
this.connect();
}, this.retryDelay);
};
this.ws.onopen = () => {
this.retryDelay = 1000; // Reset on successful connect
};
}
}Message Ordering Is Not Guaranteed
TCP guarantees ordered delivery, but between your application logic on the client and server, order can break down. If user A sends edit at position 10 and user B simultaneously sends an insert at position 8, both can arrive in an order that makes one invalid.
For simple use cases, a sequence number and server-side ordering is sufficient. For true conflict-free collaboration, look into CRDTs (Conflict-free Replicated Data Types) — complex but worth understanding conceptually.
Connection State Must Be Visible to Users
Users need to know when they are disconnected. A collaborative editor where a user types for two minutes while silently disconnected, then loses all changes on reconnect, is a terrible experience.
Maintain explicit connection state in your UI:
const [connectionState, setConnectionState] = useState('connecting');
// 'connecting' | 'connected' | 'disconnected' | 'reconnecting'Show a clear indicator — a status dot, a banner, something — whenever the state is not 'connected'. Do not let users type into a void.
Horizontal Scaling Requires Pub/Sub
A WebSocket connection is persistent and lives on one server. When you scale to multiple server instances, users connected to different servers cannot receive each other's messages without a pub/sub layer.
Redis Pub/Sub (or similar) is the standard solution: every server subscribes to a channel and publishes incoming messages to it, ensuring all connected servers broadcast to their respective clients.