DEV Community

Tamiz Uddin
Tamiz Uddin

Posted on • Originally published at tamiz.pro

Building Resilient Real-Time Systems: WebSockets, Redis, and High Availability Architectures

Originally published on tamiz.pro.

Building real-time systems that are not only fast but also resilient and highly available is a critical challenge in modern software architecture. From collaborative applications and live dashboards to gaming and IoT, the demand for instant, uninterrupted data flow is pervasive. This deep dive explores how WebSockets, for persistent bidirectional communication, and Redis, for high-performance data storage and messaging, can be combined to create robust real-time solutions capable of withstanding failures and scaling gracefully.

Table of Contents

1. The Core Challenge: Real-Time Resilience

Real-time systems are inherently complex due to their stateful nature and the need for low-latency, continuous communication. Resilience in this context means the system's ability to continue operating correctly, even in the face of partial failures, unexpected load spikes, or network disruptions. This involves ensuring data consistency, message delivery guarantees, and seamless client experience across various failure scenarios. Achieving this requires careful consideration of every layer, from client-side reconnection logic to server-side scaling and data persistence strategies.

2. WebSockets: The Foundation of Real-Time Communication

WebSockets provide a full-duplex communication channel over a single TCP connection, making them ideal for scenarios requiring persistent, low-latency, bidirectional data exchange. Unlike traditional HTTP request-response cycles, WebSockets maintain an open connection, significantly reducing overhead.

2.1. Understanding WebSocket Mechanics

The WebSocket handshake begins as an HTTP request, which is then upgraded to a WebSocket connection. Once established, both client and server can send messages independently. This persistent connection is key to real-time applications.

Consider a basic Node.js WebSocket server using the ws library:

// server.js
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}`);
    // Echo message back to client
    ws.send(`Server received: ${message}`);
  });

  ws.on('close', () => {
    console.log('Client disconnected');
  });

  ws.on('error', error => {
    console.error('WebSocket error:', error);
  });
});

console.log('WebSocket server started on port 8080');
Enter fullscreen mode Exit fullscreen mode

And a simple client:

// client.js (or browser console)
const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
  console.log('Connected to WebSocket server');
  ws.send('Hello from client!');
};

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

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

ws.onerror = error => {
  console.error('WebSocket error:', error);
};
Enter fullscreen mode Exit fullscreen mode

2.2. Scaling WebSocket Servers

A single WebSocket server can handle thousands of concurrent connections, but for true resilience and higher scale, multiple servers are essential. This introduces challenges: how do different servers communicate, and how do clients reconnect to the correct server after a disruption? This is where Redis becomes indispensable.

3. Redis: The Backbone of State and Messaging

Redis (Remote Dictionary Server) is an open-source, in-memory data structure store, used as a database, cache, and message broker. Its speed and versatility make it a perfect companion for real-time systems.

3.1. Redis as a Pub/Sub Broker

Redis's Publish/Subscribe (Pub/Sub) mechanism allows clients to subscribe to channels and receive messages published to those channels. This is fundamental for enabling communication between multiple WebSocket server instances.

Imagine multiple WebSocket servers (let's call them ws-worker-1, ws-worker-2) running behind a load balancer. When a user connected to ws-worker-1 sends a message intended for another user connected to ws-worker-2, ws-worker-1 can publish that message to a Redis channel. All ws-worker instances subscribe to this channel, and ws-worker-2 receives the message and forwards it to its connected client.

// WebSocket server instance (simplified for Redis Pub/Sub)
const redis = require('redis');
const WebSocket = require('ws');

const publisher = redis.createClient();
const subscriber = redis.createClient();

const wss = new WebSocket.Server({ port: 8080 }); // This would be dynamic in a real setup

subscriber.subscribe('chat_channel');

subscriber.on('message', (channel, message) => {
  console.log(`Received message from Redis on channel ${channel}: ${message}`);
  // Broadcast message to all connected clients on *this* server instance
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(`Broadcast from Redis: ${message}`);
    }
  });
});

wss.on('connection', ws => {
  ws.on('message', message => {
    console.log(`Received client message: ${message}`);
    // Publish client message to Redis channel for other instances
    publisher.publish('chat_channel', message.toString());
  });
});

console.log('WebSocket server with Redis Pub/Sub started.');
Enter fullscreen mode Exit fullscreen mode

This pattern ensures that messages are propagated across all active WebSocket server instances, allowing clients connected to any server to receive updates.

3.2. Redis for Session Management and Presence

Beyond messaging, Redis can store transient session data and presence information (who is online, in which room, etc.).

When a user connects, their userId and the ws-worker-id they are connected to can be stored in Redis. This allows any ws-worker to lookup a user's current connection point if direct messaging is needed. Using Redis SETS or HASHES for managing user presence in specific rooms is highly efficient.

// Storing user presence in a chat room
// On user join:
publisher.sadd('room:general:users', 'user123');
publisher.hset('user:user123:session', 'wsServerId', 'server-alpha-1');

// On user leave:
publisher.srem('room:general:users', 'user123');
publisher.hdel('user:user123:session', 'wsServerId');

// To get all users in a room:
publisher.smembers('room:general:users', (err, members) => {
  console.log(`Users in general room: ${members}`);
});
Enter fullscreen mode Exit fullscreen mode

3.3. Redis for Data Caching and Event Sourcing

Redis also serves as an excellent cache for frequently accessed data, reducing load on primary databases. In an event-sourced architecture, Redis Streams can even act as a lightweight event log, enabling real-time processing and playback of events. This is particularly useful for reconstructing application state or implementing features like

Top comments (0)