Implementing Real-Time Bi-Directional WebSockets Using Socket.io & Redis Adapter

Master real-time event-driven architecture in Node.js by implementing bi-directional WebSockets with Socket.io, scaling across multi-instance clusters using the Redis adapter.

In modern web engineering, building real-time features—such as live chat systems, collaborative document editing, real-time analytics dashboards, and instant notifications—requires maintaining persistent, bi-directional communication channels between clients and servers. Traditional HTTP request-response cycles are ill-suited for these workloads due to the overhead of repeated polling and connection establishment.

The WebSocket protocol solves this by establishing a single, low-latency, full-duplex TCP connection over which data can flow freely in both directions. However, running WebSockets in production introduces a major scaling hurdle: when your Node.js backend scales horizontally behind a load balancer across multiple container instances (e.g., in Kubernetes or ECS), clients connected to Server A cannot communicate with clients connected to Server B.

By integrating **Socket.io** with the **Redis Adapter**, you can seamlessly synchronize events across multi-instance clusters using Redis Pub/Sub messaging. This comprehensive guide explores how to configure Socket.io, scale across server nodes, and manage real-time connections efficiently.

Understanding Pub/Sub Scaling in Clustered Environments

Socket.io is an event-driven real-time engine built on top of HTTP. It provides automatic reconnection, fallback mechanisms, room-based broadcasting, and multiplexing out of the box.

When you scale your Node.js application to multiple instances, a client connected to Pod 1 cannot emit an event to a room if another target user is connected to Pod 2. The **Socket.io Redis Adapter** bridges this gap by subscribing to a Redis Pub/Sub channel. Whenever an event or broadcast is emitted on one server instance, it publishes the message to Redis, which instantly broadcasts it to all other active Node.js server nodes in the cluster.

Configuring Socket.io with the Redis Adapter

To implement scaled WebSockets, install `socket.io`, `redis`, and `@socket.io/redis-adapter`.

BASH
Installing Socket.io and Redis adapter packages.
npm install socket.io redis @socket.io/redis-adapter
TypeScript
Setting up Socket.io server with Redis adapter in Node.js.
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import { createClient } from 'redis';
import { createAdapter } from '@socket.io/redis-adapter';

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: '*',
    methods: ['GET', 'POST'],
  },
});

async function startServer() {
  // Create Redis pub/sub clients
  const pubClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });
  const subClient = pubClient.duplicate();

  await Promise.all([pubClient.connect(), subClient.connect()]);

  // Attach Redis adapter to Socket.io
  io.adapter(createAdapter(pubClient, subClient));

  io.on('connection', (socket) => {
    console.log(`Client connected: ${socket.id}`);

    socket.on('join_room', (room: string) => {
      socket.join(room);
      console.log(`Socket ${socket.id} joined room: ${room}`);
    });

    socket.on('send_message', (data: { room: string; message: string }) => {
      // Broadcast message to all clients in room across all server instances
      io.to(data.room).emit('receive_message', {
        senderId: socket.id,
        message: data.message,
        timestamp: new Date().toISOString(),
      });
    });

    socket.on('disconnect', () => {
      console.log(`Client disconnected: ${socket.id}`);
    });
  });

  const PORT = process.env.PORT || 4000;
  server.listen(PORT, () => {
    console.log(`WebSocket server running on port ${PORT}`);
  });
}

startServer().catch(console.error);

Connecting from React or Frontend Applications

On the client side, install `socket.io-client` to establish persistent WebSocket connections, join communication rooms, and listen for real-time events.

TSX
React component for real-time chat messaging using Socket.io client.
'use client';

import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';

let socket: Socket;

export default function ChatComponent() {
  const [message, setMessage] = useState('');
  const [chatLog, setChatLog] = useState<string[]>([]);
  const room = 'general-room';

  useEffect(() => {
    socket = io('http://localhost:4000');

    socket.on('connect', () => {
      console.log('Connected to WebSocket server:', socket.id);
      socket.emit('join_room', room);
    });

    socket.on('receive_message', (data) => {
      setChatLog((prev) => [...prev, `${data.senderId}: ${data.message}`]);
    });

    return () => {
      socket.disconnect();
    };
  }, []);

  const sendMessage = (e: React.FormEvent) => {
    e.preventDefault();
    if (!message.trim()) return;

    socket.emit('send_message', { room, message });
    setMessage('');
  };

  return (
    <div className="p-6 max-w-lg mx-auto bg-white rounded-xl shadow">
      <h2 className="text-xl font-bold mb-4">Real-Time Chat Room</h2>
      <div className="border p-4 h-64 overflow-y-auto mb-4 bg-gray-50 rounded">
        {chatLog.map((msg, idx) => (
          <p key={idx} className="text-sm text-gray-700 mb-1">{msg}</p>
        ))}
      </div>
      <form onSubmit={sendMessage} className="flex gap-2">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type a message..."
          className="border p-2 flex-1 rounded"
        />
        <button type="submit" className="bg-indigo-600 text-white px-4 py-2 rounded">Send</button>
      </form>
    </div>
  );
}

Summary

Implementing real-time bi-directional WebSockets using Socket.io and the Redis adapter enables Node.js applications to scale effortlessly across clustered, multi-instance server environments.

By leveraging Redis Pub/Sub to synchronize rooms and broadcast events across server instances, engineering teams can build highly resilient, low-latency real-time applications capable of handling millions of concurrent users.