NITAI IT SOLUTION

Building a Real-Time Chat & Notification System: WebSockets, Redis Pub/Sub, and Node.js

How to architect a horizontally scalable real-time messaging platform: WebSocket state management, connection heartbeats, Redis adapters, and push notifications.

In modern applications—from collaborative document editors and customer support desks to live delivery tracking and social feeds—users expect instantaneous data updates. Polling a REST endpoint every 3 seconds burns unnecessary server CPU, exhausts database connection pools, and creates perceptible lag. Modern real-time architectures utilize bidirectional WebSockets coupled with a distributed Redis Pub/Sub backbone to deliver messages with sub-20ms latency across millions of connected clients.

HTTP Polling vs. Server-Sent Events (SSE) vs. WebSockets

Selecting the right real-time communication protocol depends on data directionality and message frequency:

Technology Protocol Communication Flow Overhead & Reconnection Optimal Use Case
Short / Long Polling Client pulls from server periodically Extreme HTTP header overhead; high server load Legacy systems with zero WebSocket support
Server-Sent Events (SSE) One-way stream (Server to Client only) Very lightweight; built on HTTP with native browser auto-reconnect LLM AI token streaming, live stock ticker prices, sports scores
WebSockets (ws / wss) Full-duplex bidirectional persistent TCP channel Lowest overhead (2-byte frame header); requires custom heartbeat & cluster sync Interactive multi-user chat, multiplayer collaboration, live gaming

The Scaling Challenge: WebSockets Across Multiple Server Instances

A single Node.js process can easily maintain 20,000 idle WebSocket connections. However, in production, applications run behind load balancers across multiple container instances (Server A, Server B, Server C).

The Problem: If Alice is connected to Server A, and Bob is connected to Server B, Server A has no direct memory access to send a message to Bob!

The Architectural Solution: Redis Pub/Sub Adapter. Every server instance subscribes to a shared Redis messaging bus. When Alice sends a message to a room on Server A, Server A publishes the event to Redis, which immediately broadcasts it to Server B and C, delivering it to Bob seamlessly:

// server.ts (Production WebSocket Cluster with Redis)
import { createServer } from 'http';
import { Server } from 'socket.io';
import { createClient } from 'redis';
import { createAdapter } from '@socket.io/redis-adapter';

const httpServer = createServer();
const io = new Server(httpServer, {
  cors: {
    origin: ['https://nitaiitsolution.in'],
    methods: ['GET', 'POST'],
    credentials: true
  },
  pingInterval: 25000, // Heartbeat sent every 25 seconds
  pingTimeout: 20000   // Disconnect if no pong received within 20s
});

async function startServer() {
  const pubClient = createClient({ url: process.env.REDIS_URL });
  const subClient = pubClient.duplicate();

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

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

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

    // Join isolated conversation room
    socket.on('join_room', (roomId: string) => {
      socket.join(roomId);
      console.log(`User ${socket.id} joined room: ${roomId}`);
    });

    // Broadcast message to everyone in the room across ALL servers!
    socket.on('send_message', async (data: { roomId: string; text: string; sender: string }) => {
      // 1. Asynchronously persist message in PostgreSQL database
      await saveMessageToDatabase(data);

      // 2. Broadcast via Redis to all connected sockets in that room
      io.to(data.roomId).emit('new_message', {
        ...data,
        timestamp: Date.now()
      });
    });

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

  httpServer.listen(4000, () => console.log('WebSocket Cluster running on port 4000'));
}

startServer();

Handling Connection Resilience: Heartbeats & Exponential Reconnect

Mobile devices constantly switch between Wi-Fi and 5G cellular towers, causing silent TCP connection drops where the client believes it is connected, but the server has closed the socket.

  1. Heartbeat Pings: The server sends periodic ping packets. If the client fails to return a pong within the timeout threshold, the connection is terminated and memory reclaimed.
  2. Exponential Backoff Reconnection: The client should never flood the server with instant reconnect attempts during server restarts. Implement exponential backoff (e.g., retry after 1s, 2s, 4s, 8s up to 30s) with random jitter.
  3. Message Delivery ACKs: The sender waits for an acknowledgment packet from the server with a message UUID before marking the message as "Delivered" with double checkmarks.

Frequently Asked Questions

How do you secure WebSocket connections?

Never pass authentication tokens inside WebSocket query strings (which get logged in plain text in access logs). Instead, pass secure HttpOnly session cookies during the initial HTTP upgrade handshake, validating the JWT before permitting the connection.

Can WebSockets be load balanced by AWS Application Load Balancer (ALB)?

Yes. AWS ALB natively supports WebSockets. However, enable Sticky Sessions (Target Group Stickiness) using cookies if your clients use HTTP long-polling fallback before upgrading to native WebSockets.

What happens if a user is offline when a message is sent?

If a recipient socket is not active in the room, the backend worker detects the absence and queues a mobile push notification (via Apple APNs or Google FCM) with a preview snippet.

Need Help Implementing This in Production?

Our senior engineering team helps startups and enterprises scale web applications, migrate cloud infrastructure, and build custom AI workflows with guaranteed delivery timelines.

Schedule Free Technical Consultation Chat with Lead Architect