How to Scale WebSockets Without It Falling Over
Scaling WebSockets breaks most backends because connections are stateful. Here is how to scale WebSocket infrastructure past one node without losing sessions.
Scaling WebSockets comes down to one uncomfortable fact: connections are stateful, and state does not spread across servers for free. A single node holds the socket, holds the session, and knows who is connected. Add a second node and those two boxes now have to agree on things they cannot see. That is where most real-time apps break, and it happens the first time traffic gets real.
The good news is that this is a solved problem if you design for it from the start. The bad news is that you cannot bolt the solution on after the fact without a rewrite.
Why one node is never enough
A WebSocket connection lives in a process. That process holds the memory for the session, the subscription list, and the presence state. It works beautifully until you outgrow one machine or you deploy new code.
The moment you have two nodes, a message that arrives on node A needs to reach a client connected to node B. Node A has no idea that client exists. Now you need a way to broadcast across nodes. And when you deploy, restarting a node drops every socket it was holding, which means every user reconnects at once, which is a thundering herd aimed at whatever is left standing.
Vertical scaling delays this. A bigger box holds more sockets. But you hit a ceiling, and the ceiling arrives with no warning because socket memory is nonlinear under load. Betting your architecture on one large machine is the same mistake as betting your business on a server you do not control.
The pub/sub backbone
The core pattern for scaling WebSockets horizontally is a pub/sub layer sitting behind your nodes. When a message needs to go out, a node publishes it to a channel. Every node subscribed to that channel receives it and forwards it to the local sockets that care. No node needs to know where a client physically lives.
Redis pub/sub is the common starting point. It is fast, simple, and gets you surprisingly far. As you grow you may move to a dedicated message broker for durability, because raw pub/sub is fire and forget: if a node is momentarily behind, the message is gone. That tradeoff between speed and durability is a decision you make on purpose, not one you discover in an incident.
The key discipline is that no node holds authoritative state that another node needs synchronously. Nodes should be able to fail, restart, and rejoin without the cluster caring. If losing a node corrupts the picture, you have not scaled, you have just added more single points of failure.
Presence is the hard part
Broadcasting messages is the easy half. Knowing who is online across a fleet of nodes is the half that eats teams alive.
Presence means every node has to contribute its local view of connected users to a shared picture, keep it fresh, and expire it fast when a connection drops silently. Sockets do not always close cleanly. A phone goes into a tunnel and the connection is dead, but your server does not know for thirty seconds. Multiply that across a cluster and your presence data is a field of ghosts.
The fix is heartbeats and short-lived presence records that must be renewed. If a node stops renewing a user's presence, that user ages out. It sounds simple and it is fiddly to get right at scale, which is why I argue for building it once in a governed foundation rather than reassembling it per product.
Surviving deploys and reconnection
A cluster that cannot deploy without dropping everyone is not production infrastructure. The pattern is graceful draining: stop accepting new connections on a node, let existing clients reconnect to healthy nodes, then restart the drained one. Clients need reconnection logic with backoff and jitter so they do not all slam back at the same millisecond.
State that matters must survive the reconnect. If a user reconnects and their session is gone, the socket working perfectly does not help them. This is why message delivery, presence, and session state have to be designed together, not stacked as three unrelated services.
Do not build all of this yourself
Every piece here is a known pattern, and stitching them into one coherent system that survives load is a real engineering project. It is the exact project AltoHost exists to absorb: the connection lifecycle, the pub/sub backbone, presence, graceful deploys, and reconnection, owned as one set of guarantees so your app assumes them instead of rebuilding them. For the same reason I make infrastructure decisions I can control, I would rather stand on a real-time layer that has already met load than one about to meet it for the first time.
Closing
Scaling WebSockets is not about a faster socket library. It is about designing so that no node is precious, state lives in a shared backbone, presence expires cleanly, and deploys drain instead of drop. Get those right and adding capacity adds capacity. Get them wrong and every new node adds a new way to fail.