Why WebSockets Still Win for Real-Time
Server-Sent Events (SSE) and long-polling have their place. But for truly bidirectional, low-latency real-time communication — collaborative editing, live dashboards, multiplayer features, IoT device telemetry — WebSockets remain the best tool. Node.js's non-blocking event loop makes it uniquely suited to managing massive numbers of concurrent socket connections.
The Architecture
Our production WebSocket infrastructure for a logistics tracking client handles up to 120,000 concurrent connections at peak (4,800 vehicles reporting GPS every 10 seconds, multiplied by ~25 dashboard viewers per vehicle on average). Here is the stack: Node.js 20 LTS + uWebSockets.js (10–20x faster than Socket.io for raw throughput), Redis Pub/Sub for fan-out across multiple Node processes, PM2 for cluster mode (1 process per vCPU), AWS Application Load Balancer with sticky sessions, and ElastiCache Redis for shared state.
Horizontal Scaling with Redis Pub/Sub
A single Node.js process handles roughly 10,000–30,000 concurrent connections comfortably before CPU becomes the bottleneck (heavily dependent on message processing complexity). To scale beyond this, run multiple processes on multiple machines and use Redis Pub/Sub as the message bus. When a message needs to be broadcast to a room or user, publish it to Redis. All Node processes subscribed to that channel receive and forward it to their locally connected sockets. This pattern scales linearly — add more Node instances behind the load balancer as traffic grows.
Memory Management: The Long-Tail Problem
The most common failure mode for high-connection WebSocket servers is memory leaks — event listener accumulation, uncleaned room memberships on disconnect, and retained references in closure scope. Our defensive patterns: (1) Always clean up event listeners in the disconnect handler. (2) Use WeakMap/WeakSet for connection-keyed state where possible. (3) Run memory profiling (clinic.js heap) in staging under simulated load before every release. (4) Set --max-old-space-size and monitor heap usage in production via Prometheus + Grafana.
Testing Real-Time at Scale
Load-testing WebSocket servers requires different tooling from standard HTTP load testing. We use k6 with the xk6-websockets extension to simulate realistic connection patterns — not just peak concurrent connections, but the ramp-up, the message frequency distribution, and the disconnect/reconnect churn that happens in real user populations. Our load tests revealed a subtle starvation issue at 80,000 connections that only appeared under realistic churn — it would never have been caught with synthetic static-load tests.