Everything we've built so far runs on one server process. This works great during development, and even for many small production apps. But once traffic grows enough that one server is not enough, Socket.IO introduces two problems that a typical stateless REST API does not have to deal with. This post explains both, before we solve them with the Redis Adapter in the next post.
Problem 1: In-Memory State Does Not Cross Server Boundaries
- Think back to everything we built: the
onlineUsersmap, rooms created withsocket.join(), the online count. All of this lives in the memory of one running Node.js process. - Now imagine you run two instances of your server, behind a load balancer, to handle more traffic. Client A connects and lands on Server 1. Client B connects and lands on Server 2. If Client A sends a chat message, and your code does
io.emit("newMessage", data), that only reaches clients connected to Server 1, sinceioonly knows about its own process's connections. Client B, sitting on Server 2, never receives it, even though both are technically using the same application. - This is the core problem: rooms, online user tracking, and broadcasts are all scoped to a single process by default. Running multiple server instances does not automatically make them aware of each other.
Problem 2: The Sticky Sessions Requirement
- Recall from Phase 2 how a Socket.IO connection starts, with HTTP long-polling, and optionally upgrades to WebSocket afterward. Long-polling works through a sequence of separate HTTP requests, all tied together using a session ID.
- Here is where multi-server setups create a subtle but serious issue. If a load balancer distributes each incoming HTTP request to a different server, purely round-robin style, a client's long-polling requests could land on a different server instance every time. Since each server instance has no idea about sessions that started on another instance, the connection breaks entirely.
- The standard fix for this is called sticky sessions (also called session affinity). This means configuring your load balancer so that once a client's first request lands on a particular server, every following request from that same client keeps going to that exact same server, instead of being spread around.
Why Not Just Sync State Between Servers Automatically?
It's reasonable to wonder why Socket.IO doesn't just handle all of this transparently for you. Technically, it's possible to synchronize connection state between every server instance so that sticky sessions are not required at all. Socket.IO's own documentation is direct about why this isn't done by default: constantly synchronizing this state across every instance introduces a significant performance cost. So instead, sticky sessions remain the recommended approach, and any cross-server broadcasting is handled separately, through an adapter, which we cover next.
The Three Pieces of a Multi-Server Setup
Putting this together, a properly scaled Socket.IO deployment needs three things working together:
- A load balancer, distributing incoming connections across multiple server instances
- Sticky sessions, configured at the load balancer level, so a client's requests consistently reach the same server instance
- An adapter, so that when one server broadcasts an event, it actually reaches clients connected to every other server instance too, not just its own
Without all three, some part of your real-time functionality will quietly break under load, sometimes in ways that are hard to notice until you're already scaled up and users start reporting missing messages.
A Realistic Limit Worth Knowing
Even a single, well-optimized Node.js server has a practical ceiling on how many concurrent Socket.IO connections it can comfortably handle, generally somewhere in the range of tens of thousands, depending on your hardware and what else that process is doing. Beyond that point, things like memory pressure and event loop contention start degrading performance in ways that are hard to predict. This is exactly the point where scaling to multiple servers, and consequently dealing with the two problems in this post, becomes necessary rather than optional.
Where This Leaves Us
To summarize the gap: sticky sessions solve the connection routing problem, making sure a client consistently reaches the same server. But they do nothing to solve the broadcasting problem, a message from a client on Server 1 still cannot reach a client on Server 2 on its own. That second problem is solved by an adapter, and the most common choice for this is Redis, which is exactly what we cover in the next post.
Summary
- Running multiple Socket.IO server instances breaks two things by default: in-memory state (rooms, online users) does not cross server boundaries, and broadcasts only reach clients on the same server that triggered them
- Sticky sessions solve connection routing, ensuring a client's requests consistently land on the same server instance, which is required for long-polling to function correctly
- Sticky sessions alone do not solve cross-server broadcasting, that requires a separate mechanism
- A full multi-server setup needs a load balancer, sticky sessions, and an adapter working together
- A single server has a practical connection ceiling, which is usually the actual trigger for needing to scale in the first place
In the next post, we introduce the Redis Adapter, the standard solution that lets multiple Socket.IO server instances broadcast events to each other's connected clients.
No comments:
Post a Comment