Building Real-Time Features with WebSockets: The Good, Bad, and Ugly

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:

  1. Start small
  2. Use a library
  3. Plan for scale
  4. Monitor everything
  5. Have fallbacks

Real-time is powerful. Just know what you're getting into.

Subscribe to Blyss Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe