Table of Contents

Share this post

Building Scalable WebSocket Applications
Backendยทยท5 min readยท7,258 views

Building Scalable WebSocket Applications

Patterns and practices for building real-time applications with WebSockets at scale.

Building Scalable WebSocket Applications

Real-time features are expected in modern applications. Here's how to build WebSocket systems that scale.

Why WebSockets?

Traditional HTTP:

  • Client polls server for updates
  • High latency
  • Wasteful bandwidth

WebSockets:

  • Persistent bi-directional connection
  • Real-time updates
  • Efficient communication

Basic WebSocket Server

Node.js with ws

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Client connected');
  
  ws.on('message', (message) => {
    console.log('Received:', message);
    ws.send(`Echo: ${message}`);
  });
  
  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

Client-side

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
  console.log('Connected');
  ws.send('Hello Server!');
};

ws.onmessage = (event) => {
  console.log('Received:', event.data);
};

ws.onclose = () => {
  console.log('Disconnected');
};

ws.onerror = (error) => {
  console.error('Error:', error);
};

Scalability Challenges

Problem 1: Multiple Server Instances

With load balancing, users connect to different servers:

User A โ†’ Server 1
User B โ†’ Server 2

User A sends message โ†’ How does User B receive it?

Solution: Redis Pub/Sub

const Redis = require('ioredis');
const publisher = new Redis();
const subscriber = new Redis();

// Subscribe to messages
subscriber.subscribe('chat');
subscriber.on('message', (channel, message) => {
  // Broadcast to local WebSocket clients
  broadcastToLocalClients(message);
});

// When receiving message from client
ws.on('message', (message) => {
  // Publish to all servers
  publisher.publish('chat', message);
});

Problem 2: Connection Management

Track which users are connected:

const connections = new Map();

wss.on('connection', (ws, req) => {
  const userId = authenticateUser(req);
  connections.set(userId, ws);
  
  ws.on('close', () => {
    connections.delete(userId);
  });
});

// Send to specific user
function sendToUser(userId, message) {
  const ws = connections.get(userId);
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(message);
  }
}

Production Patterns

1. Authentication

const jwt = require('jsonwebtoken');

wss.on('connection', (ws, req) => {
  const token = req.url.split('token=')[1];
  
  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    ws.userId = user.id;
  } catch (err) {
    ws.close(4001, 'Unauthorized');
    return;
  }
  
  // Connection is authenticated
});

2. Heartbeat/Ping-Pong

Keep connections alive:

function heartbeat() {
  this.isAlive = true;
}

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', heartbeat);
});

// Check every 30 seconds
setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      return ws.terminate();
    }
    
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

3. Message Queuing

Handle bursts of messages:

const queue = [];
let processing = false;

ws.on('message', (message) => {
  queue.push(message);
  processQueue();
});

async function processQueue() {
  if (processing) return;
  processing = true;
  
  while (queue.length > 0) {
    const message = queue.shift();
    await handleMessage(message);
  }
  
  processing = false;
}

4. Room-based Broadcasting

const rooms = new Map();

function joinRoom(ws, roomId) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId).add(ws);
  ws.currentRoom = roomId;
}

function broadcastToRoom(roomId, message) {
  const clients = rooms.get(roomId);
  if (!clients) return;
  
  clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

ws.on('close', () => {
  if (ws.currentRoom) {
    rooms.get(ws.currentRoom).delete(ws);
  }
});

Using Socket.io

Socket.io adds convenience features:

const io = require('socket.io')(3000);

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  // Join room
  socket.on('join', (room) => {
    socket.join(room);
  });
  
  // Broadcast to room
  socket.on('message', (room, msg) => {
    io.to(room).emit('message', msg);
  });
  
  // Direct message
  socket.on('private', (recipientId, msg) => {
    io.to(recipientId).emit('message', msg);
  });
  
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

Monitoring and Debugging

Connection Metrics

let connectionCount = 0;
let messageCount = 0;

wss.on('connection', (ws) => {
  connectionCount++;
  console.log(`Connections: ${connectionCount}`);
  
  ws.on('message', () => {
    messageCount++;
  });
  
  ws.on('close', () => {
    connectionCount--;
  });
});

// Report metrics
setInterval(() => {
  console.log({
    connections: connectionCount,
    messages: messageCount,
    memoryUsage: process.memoryUsage()
  });
}, 60000);

Error Handling

ws.on('error', (error) => {
  console.error('WebSocket error:', error);
  // Don't crash the server
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
  // Log but don't crash
});

Load Testing

Use tools to test WebSocket performance:

npm install -g artillery

artillery quick --count 100 --num 50 ws://localhost:8080

Best Practices

  1. Always authenticate WebSocket connections
  2. Implement heartbeats to detect dead connections
  3. Use Redis for multi-server setups
  4. Rate limit messages per connection
  5. Validate all incoming messages
  6. Monitor connection counts and message rates
  7. Implement graceful shutdown
  8. Use binary frames for large payloads
  9. Add reconnection logic on client
  10. Log errors, don't crash

Conclusion

Building scalable WebSocket applications requires:

  • Proper connection management
  • Redis for cross-server communication
  • Heartbeat mechanisms
  • Error handling
  • Monitoring

Start simple, add complexity as you scale. Real-time doesn't have to be complicated.

Comments (0)

Loading comments...