WebSocket Scaling Best Practices for High-Traffic Production Apps
Practical WebSocket scaling best practices: sticky sessions, horizontal scaling, Redis Pub/Sub, load balancing, and capacity planning for real-time systems.
A WebSocket prototype that works perfectly on one server starts showing cracks the moment you add a second instance. Messages go missing. Presence flickers. Some users never receive events. These are not bugs in your business logic — they are predictable consequences of stateful persistent connections meeting a horizontally scaled deployment. Here are the practices that prevent them.
Why WebSocket Scaling Best Practices Differ from Standard HTTP
HTTP requests are stateless. Any server can handle any request. WebSocket connections are stateful — a client is pinned to one server for the lifetime of the connection. That single fact changes almost every operational decision: load balancing strategy, connection draining, failover behavior, and how you broadcast events.
1. Sticky Sessions Are Not Optional
When a client connects to a load balancer, it must consistently reach the same backend server for every subsequent WebSocket frame. Without sticky sessions, the load balancer may route frames to a different server that has no knowledge of that connection.
Configure your load balancer to use IP hash or cookie-based affinity. On NGINX:
upstream websocket_backends {
ip_hash;
server app1:3000;
server app2:3000;
}On AWS ALB, enable stickiness on the target group with a session cookie. On Kubernetes, use a service with sessionAffinity: ClientIP or an ingress controller that supports sticky sessions.
Sticky sessions do not solve the multi-server state problem — they only ensure a client's frames reach one server consistently. State sharing still requires a message broker.
2. Use a Message Broker to Decouple Servers
When server A needs to send an event to a client connected to server B, it cannot do so directly. A broker solves this.
Redis Pub/Sub is the most common choice for moderate scale. Each server subscribes to relevant channels. When server A wants to broadcast to room project:42, it publishes to the room:project:42 channel. Every subscribed server receives the message and delivers it to local clients in that room.
// Publish from any server
await redis.publish('room:project:42', JSON.stringify(payload));
// Each server subscribes and delivers locally
subscriber.on('message', (channel, raw) => {
const roomId = channel.replace('room:', '');
broadcastToLocalRoom(roomId, raw);
});For higher throughput, Kafka or NATS are better choices. Redis Pub/Sub is fire-and-forget — if no subscriber is listening, the message is lost. For durable delivery, use Redis Streams instead.
3. Separate the WebSocket Gateway from Business Logic
A common architecture mistake is putting heavy computation in the same process that manages WebSocket connections. When the event loop blocks, connections stall and clients timeout.
Run a thin WebSocket gateway process whose only jobs are: accept connections, authenticate, maintain the in-memory connection map, and route messages. Business logic — database writes, external API calls, event processing — lives in separate workers that communicate with the gateway via the message broker.
This lets you scale the gateway and the workers independently. You can also restart workers without dropping connections.
4. Set Connection Limits and Backpressure
An unbounded server will accept connections until it runs out of file descriptors or memory. Set an explicit limit and return a proper error when the limit is reached:
const MAX_CONNECTIONS = 5000;
wss.on('connection', (socket) => {
if (wss.clients.size > MAX_CONNECTIONS) {
socket.close(1013, 'Server at capacity');
return;
}
// proceed
});The client-side should treat a 1013 close code as a signal to retry with exponential backoff, not to immediately reconnect and worsen the overload.
5. Plan Your Horizontal Pod Autoscaling Carefully
WebSocket servers cannot scale down as freely as stateless HTTP services. When Kubernetes terminates a pod, all connections on that pod drop. Clients reconnect, but they land on other pods — causing a thundering herd.
Mitigate this by:
- Draining connections gracefully — set a pre-stop hook that stops accepting new connections and waits for clients to reconnect naturally before the pod terminates
- Staggering scale-down events — scale down one pod at a time, not multiple simultaneously
- Exponential backoff on reconnect — clients must not all reconnect in the same second
6. Monitor the Right Metrics
Standard request/second metrics miss most WebSocket issues. Track:
- Active connection count per instance — watch for uneven distribution indicating sticky session failures
- Message throughput — frames sent and received per second
- Connection churn rate — rapid connect/disconnect cycles indicate client-side instability
- Heartbeat failure rate — high rates indicate network problems or server overload
- Queue depth in the message broker — lag here means servers are not consuming fast enough
Set alerts on connection count spikes and message broker lag, not just CPU and memory.
7. Handle Connection Draining Before Deploys
Zero-downtime deploys require a draining period. Before terminating an old instance, stop routing new connections to it and give existing clients a graceful close signal so they can reconnect to other instances. A 1001 Going Away close code signals the client that reconnection is expected.
Build your client reconnection logic to treat 1001 as a normal event, not an error.
8. Capacity Planning Baseline
Rough numbers for planning:
- A single Node.js process handles roughly 10,000 to 50,000 concurrent WebSocket connections depending on message rate and payload size
- Redis Pub/Sub handles hundreds of thousands of messages per second on a single instance
- Memory consumption per idle connection is typically in the range of a few kilobytes
Benchmark under your specific message patterns before committing to an instance size.
Building a WebSocket infrastructure that scales reliably across instances is an architecture problem, not just a configuration one. If you are designing a real-time system that needs to handle meaningful traffic, Clixo can design and ship it.