Building Real-Time Features with WebSockets: The Good, Bad, and Ugly
Building Real-Time Features with WebSockets: The Good, Bad, and Ugly
We added real-time notifications to our app. Users loved it. Our infrastructure team? Not so much.
Here's what we learned building WebSocket features at scale.
The Promise
WebSockets enable true real-time communication. No polling. No delays. Just instant updates.
The pitch is compelling: users see changes immediately. Collaborative features become possible. The app feels alive.
The Reality
WebSockets are harder than they look.
What We Built
Our app needed:
- Real-time notifications
- Live activity feeds
- Collaborative document editing
- Presence indicators (who's online)
We started simple: a WebSocket server in Node.js with Socket.io.
The Good Parts
Instant Updates
Users love instant notifications. No refresh needed. Changes appear immediately.
Implementation is straightforward:
// Server
io.on('connection', (socket) => {
socket.on('subscribe', (userId) => {
socket.join(`user:${userId}`);
});
});
function notifyUser(userId, message) {
io.to(`user:${userId}`).emit('notification', message);
}
// Client
socket.on('notification', (message) => {
showNotification(message);
});
Reduced Server Load
No more polling every 5 seconds. WebSockets use one persistent connection instead of thousands of HTTP requests.
Our API request count dropped 40%.
Better UX
Collaborative features feel magical. Multiple users editing the same document, seeing each other's changes in real-time.
The Bad Parts
Scaling Is Hard
HTTP is stateless. WebSockets are stateful. This breaks everything you know about scaling web apps.
With HTTP, any server can handle any request. With WebSockets, users are connected to specific servers.
We needed:
- Sticky sessions (route users to the same server)
- Redis pub/sub (broadcast messages across servers)
- Connection state management
Connection Management
WebSocket connections drop. A lot. Mobile networks are unreliable. Users close laptops. Connections timeout.
We implemented:
- Automatic reconnection
- Exponential backoff
- Connection state tracking
- Message queuing for offline users
let reconnectAttempts = 0;
socket.on('disconnect', () => {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
setTimeout(() => {
reconnectAttempts++;
socket.connect();
}, delay);
});
socket.on('connect', () => {
reconnectAttempts = 0;
});
Authentication
HTTP authentication is simple: send a token with each request. WebSocket authentication is trickier.
We authenticate on connection:
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (isValidToken(token)) {
socket.userId = getUserIdFromToken(token);
next();
} else {
next(new Error('Authentication failed'));
}
});
But tokens expire. We needed token refresh logic over WebSockets.
Load Balancing
Standard load balancers don't work well with WebSockets. We needed:
- Layer 7 load balancing
- WebSocket-aware health checks
- Proper timeout configuration
The Ugly Parts
Memory Leaks
Each WebSocket connection consumes memory. With 10,000 concurrent users, memory usage exploded.
We had to:
- Implement connection limits
- Add memory monitoring
- Clean up disconnected sockets properly
Debugging
Debugging WebSocket issues is painful. No request logs. No easy way to replay issues. State is distributed across connections.
We built custom debugging tools:
- Connection state dashboard
- Message logging
- Connection lifecycle tracking
Testing
Testing real-time features is hard. You need to simulate:
- Multiple concurrent connections
- Network failures
- Race conditions
- Message ordering
Our test suite is complex and slow.
Lessons Learned
1. Use a Library
Don't use raw WebSockets. Use Socket.io or similar. They handle:
- Reconnection
- Fallbacks (long-polling)
- Room management
- Broadcasting
2. Plan for Scale
WebSockets don't scale like HTTP. Plan for:
- Redis pub/sub for multi-server setups
- Connection limits per server
- Graceful degradation
3. Monitor Everything
Track:
- Active connections
- Connection duration
- Message rate
- Memory per connection
- Reconnection rate
4. Have Fallbacks
WebSockets might not work (corporate firewalls, old browsers). Implement fallbacks:
- Long-polling
- Server-sent events
- Regular polling as last resort
5. Don't Overuse
Not everything needs to be real-time. We use WebSockets for:
- Notifications
- Live feeds
- Collaborative features
Everything else uses regular HTTP. It's simpler.
Our Architecture
- Node.js servers: Handle WebSocket connections
- Redis: Pub/sub for cross-server messaging
- PostgreSQL: Store messages for offline users
- Load balancer: Sticky sessions, WebSocket-aware
Would We Do It Again?
Yes, but with realistic expectations. WebSockets enable great features, but they're complex.
If you're adding real-time features:
- Start small
- Use a library
- Plan for scale
- Monitor everything
- Have fallbacks
Real-time is powerful. Just know what you're getting into.