Module 6 · Lesson 3 of 4
WebSockets across many servers
Sticky connections, pub/sub fan-out and reconnects.
The live seat map works perfectly on your laptop. In production, Gatepass runs ten API servers behind a load balancer, and fans start reporting that seats “sold out of nowhere”. A booking on server 3 updates the phones connected to server 3, and nobody else. HTTP servers could forget each request; a WebSocket server can't forget its connections.
Why one server was easy
With one server, the booking handler and every open socket live in the same process. When seat C14 sells, the handler loops over the sockets watching event 42 and writes to each one. An in-memory map from event id to connections is all you need.
Add a second server and that map is split in two. The load balancer sent each phone's upgrade request to whichever server it picked, and the socket stays there for its whole life. The booking request, a separate HTTP call, lands on any server. The server that saves the booking usually doesn't hold most of the phones that need to hear about it.
Fan out through pub/sub
The fix is to stop treating “who needs to know” as one server's problem. Put a publish/subscribe channel between the servers. Redis pub/sub is the common first choice; NATS, or a managed service, do the same job.
- When a phone subscribes to event 42, its server subscribes to the channel
event:42(once, no matter how many of its phones watch that event). - When any server commits a booking, it publishes
{seat: "C14", v: 1833}toevent:42. - Redis delivers the message to every server subscribed to that channel.
- Each server writes it to its own local sockets for event 42.
The booking server no longer needs to know where the phones are. Servers can come and go, and each one only cares about its own connections.
Redis pub/sub is fire-and-forget: a server that's disconnected from Redis at the moment of publishing misses the message, and nothing is stored. That's acceptable here because the seat map has a second line of defense, the version cursor. Any server or phone that notices a gap (it has v1831 and receives v1833) fetches the changes since 1831 from the database. If you can't tolerate gaps at all, publish to a durable log such as a Redis stream or Kafka instead. Module 15 covers those.
Try it: fan-out and a restart
Book a few seats with pub/sub off and count the stale phones. Turn it on and book again. Then restart server 2 and watch where its phones go.
Try it
One booking, nine phones, three servers
Book a seat and see which phones hear about it. Then turn on pub/sub, and try restarting a server in the middle of the on-sale.
Up to date
9/9
seat map v1832
Stale
0
showing sold seats as free
Reconnecting
0
- 9 phones connected, 3 per server. Everyone has seat map v1832.
Sticky connections and uneven load
A WebSocket is sticky by nature: once upgraded, the TCP connection is pinned to one server. You don't need session affinity settings for that. You do need to know what it does to your load.
- Load spreads at connect time, not per message. A server that was restarted comes back empty, while the others stay full until phones naturally disconnect. Spreading connections with least-connections balancing helps new connections, not old ones.
- Scale on connections, not just CPU. A WebSocket server can sit at 15% CPU and still be at its memory or file-descriptor limit. Autoscale on open connections per server.
- Deploys become events. Rolling restarts drop thousands of connections at once. Drain a server by closing its sockets a few hundred at a time, and send clients a “please reconnect” message rather than letting them discover the drop.
Reconnects, done properly
A dropped connection is normal, not exceptional: phones change networks, laptops sleep, servers deploy. Every client needs the same reconnect routine.
- Wait a random delay, growing with each failure (for example, a random time up to 1 s, then up to 2 s, 4 s, capped at 30 s). If 40,000 phones lost the same server, you don't want 40,000 upgrades in the same 100 ms. The retries lesson explains why the randomness matters.
- Reconnect through the load balancer, which picks a healthy server.
- Resubscribe with the last version seen, so the server can replay what was missed.
- Fall back to a full refresh if the gap is too old to replay (say, more than 5 minutes or 1,000 versions).
Your turn
For a big on-sale, Gatepass expects 300,000 fans with the seat map open at once, and each WebSocket server can hold 40,000 connections. Bookings peak at 400 a second, spread across 50 events. What would you check or decide?
Stuck? Count servers, then count pub/sub messages in and out.
Key takeaways
- WebSocket connections are pinned to one server, but bookings can happen on any server, so a booking must be broadcast to every server.
- A pub/sub channel per event lets each server subscribe for its own phones and lets any server publish.
- Plain pub/sub can lose messages. A version number on every update lets servers and phones detect gaps and fetch what they missed.
- Load balances at connect time. Scale and monitor on open connections, and drain servers gently on deploy.
- Clients reconnect with random, growing delays and resubscribe with the last version they saw.
Finished reading?
Mark it complete to tick it off in the course.