Scalable WebSockets: Socket.IO & Redis Adapter
A single Node.js WebSocket process running Socket.IO can comfortably handle 10,000 concurrent client TCP connections on a standard cloud VM. However, as user traffic expands to millions of real-time connections, a single server hits memory limits (max-old-space-size) and CPU bottlenecks.
Scaling WebSockets horizontally across multiple Node.js instances introduces a critical synchronization problem: if User A is connected to Server Node 1 and User B is connected to Server Node 2, Server Node 1 cannot route messages to User B directly because User B's TCP socket lives on Node 2.
Socket.IO Redis Adapter solves cross-node communication by leveraging Redis Pub/Sub. When Server Node 1 emits an event (io.to('room-101').emit('chat', msg)), the Redis Adapter publishes the event to Redis, which immediately broadcasts it to all other Node.js instances in the cluster. This guide details horizontally scaled WebSocket gateways, Nginx sticky sessions, and connection heartbeats.
Mental Model: Single-Node WebSocket Servers vs Horizontally Scaled Redis Pub/Sub Gateways
Single-node WebSocket architectures keep all active TCP socket instances in local Node.js memory. When scaling to 10 instances, emitting a message to a shared room only reaches clients connected to that specific instance.
Horizontally Scaled Socket.IO Redis Architecture separates connection state from message broadcasting:
1. Stateful TCP Sockets: Distributed across N Gateway Pods behind an HTTP/WebSocket Load Balancer. 2. Redis Pub/Sub Bus: Interconnects all Gateway Pods. Emitting a room event on any pod publishes a Redis message that instantly triggers socket delivers across all gateway nodes. For real-time state synchronization, review real time collaborative apps crdts websockets and prevent cache stampede redis.
Quick reference
- Horizontally scales real-time WebSocket connection gateways across dozens of Kubernetes pods.
- Redis Pub/Sub inter-node messaging bus broadcasts events to all cluster gateway instances in <5ms.
- Socket.IO Rooms feature routes messages selectively to specific client subsets across pods.
- Allows independent scaling of WebSocket connection nodes from background worker tasks.
- Powers real-time chat, collaborative editors, gaming, and trading dashboards across enterprise fleets.
Remember this
Implement Socket.IO Redis Adapter to scale WebSocket gateways horizontally across multi-pod clusters.
Redis Pub/Sub Streams Adapter & Cross-Node Event Broadcasting
Configuring the @socket.io/redis-adapter requires establishing dual Redis connection clients (one publisher, one subscriber):
1import { Server } from "socket.io";2import { createClient } from "redis";3import { createAdapter } from "@socket.io/redis-adapter";4 5const pubClient = createClient({ url: "redis://redis-cluster:6379" });6const subClient = pubClient.duplicate();7 8await Promise.all([pubClient.connect(), subClient.connect()]);9 10const io = new Server(3000, {11 adapter: createAdapter(pubClient, subClient)12});13 14io.on("connection", (socket) => {15 socket.join("chat-room-42");16});Quick reference
- Requires two distinct Redis TCP connections: one for publishing events and one for subscribing.
- Automatically serializes Socket.IO events, binary buffers, and room IDs over Redis channels.
- Supports Redis Cluster and Sentinel configurations for high availability and automatic failover.
- Redis Streams adapter option provides durable at-least-once message delivery for offline clients.
- High performance: Redis handles over 100,000 pub/sub message broadcasts per second per node.
Remember this
Connect dual Redis pub/sub clients to Socket.IO to enable transparent cross-pod event broadcasting.
Sticky Sessions (Nginx / Cloud Load Balancers) vs HTTP Long-Polling Fallbacks
Socket.IO begins client connection handshakes using HTTP long-polling before upgrading to a WebSocket TCP stream (Upgrade: websocket).
### Why Sticky Sessions Are Mandatory
If an HTTP load balancer (Nginx, AWS ALB) routes initial long-polling HTTP requests (GET /socket.io/?transport=polling&sid=XYZ) to Node 1 and the follow-up handshake HTTP request to Node 2 before WebSocket upgrade completes, Node 2 will reject the unknown session ID (Session ID unknown).
Nginx Sticky Session Configuration:
1upstream websocket_backend {2 ip_hash; # Ensures HTTP requests from same client IP land on same node!3 server node1.internal:3000;4 server node2.internal:3000;5}Quick reference
- Initial HTTP long-polling handshake requires sticky sessions (ip_hash or session cookies).
- Prevents 'Session ID unknown' HTTP 400 errors during WebSocket connection upgrade phases.
- AWS ALB / GCP Cloud Load Balancing support Cookie-based stickiness for Socket.IO ingress.
- Clients configured with transports: ['websocket'] skip HTTP long-polling entirely to bypass stickiness.
- Provides resilient connection fallback for environments blocking native WebSocket TCP ports.
Remember this
Configure Nginx ip_hash or ALB session cookies to maintain sticky sessions during WebSocket handshakes.
Connection Heartbeats, Disconnect Recovery, & Presence Tracking
Unannounced network drops (e.g. mobile client entering a tunnel) leave orphaned TCP sockets on servers indefinitely. Socket.IO uses Connection Heartbeats (pingInterval: 25000, pingTimeout: 20000) to detect silently dropped connections.
### User Presence Tracking via Redis Hashes To track online users across the cluster, store user session IDs in Redis Hashes with TTL expiration:
1io.on("connection", (socket) => {2 const userId = socket.handshake.auth.userId;3 await redis.hset(`presence:users`, userId, socket.id);4 5 socket.on("disconnect", async () => {6 await redis.hdel(`presence:users`, userId);7 });8});Quick reference
- Ping/Pong heartbeats clean up dead TCP sockets automatically during unannounced client drops.
- Redis Hashes track real-time global online user presence across all cluster gateway pods.
- Disconnect recovery buffers allow clients to rejoin rooms and fetch missed messages within 2 minutes.
- Prevents memory leaks caused by lingering zombie TCP socket objects in Node.js RAM.
- Ensures accurate online status indicators for high-scale enterprise chat and social apps.
Remember this
Combine Socket.IO ping heartbeats with Redis Hashes for automatic socket cleanup and user presence tracking.
Key takeaway
To test Socket.IO Redis scaling, launch 2 Node.js instances locally on ports 3001 and 3002 connected to Redis. Connect a client to 3001 and emit an event to a client connected to 3002.
Related Articles
Explore this topic