Skip to the solution
MediumSystem Design13 min read

WebSocket presence, sticky sessions, and resume — do you need sticky LB?

308 views

The problem

Question presented to candidate: "We run a chat product on a fleet of WebSocket servers behind a load balancer. Product wants an accurate online indicator, and when a phone drops off the network for a few seconds it should reconnect and get the messages it missed. Do we need sticky sessions on the load balancer to make that work? Where does the state live?"

What a strong answer should cover:

  • An open WebSocket is already pinned to one server for its whole life; stickiness only matters for the next connection (a reconnect) or for transports made of several HTTP requests (long-polling fallbacks such as Socket.IO's, which its docs say require sticky sessions)
  • Keeping resume buffers or presence in server memory makes correctness depend on routing and on that server staying alive; externalising them (presence store with TTLs, a per-conversation log with sequence numbers) lets any server serve any reconnect
  • Presence as heartbeats with a TTL, and why a silent disconnect is only detected when the TTL expires; multi-device presence; the cost of fanning presence changes out
  • Resume: the client sends its last sequence number, the server replays from a durable log, and falls back to a full resync when the gap is too old
  • Stickiness as an optimisation (warm caches, fewer cross-node hops), and why hash-mod-N stickiness reshuffles users when the fleet changes

Clarifying questions expected:

  • How long may a client be gone and still expect a seamless resume (seconds, minutes, days)?
  • Is presence per user or per device, and who needs to see it (friends, a room, everyone)?
  • Do clients ever fall back to HTTP long polling?

Code / implementation expected: Optional — a design with the state placed explicitly; the example shows resume succeeding or failing under each choice.

  • websocket
  • presence
  • scaling

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

13 min read · 12 sections

Read the code

Resume and presence across 3 WebSocket servers: in-memory buffers vs a shared log, sticky vs round-robin
// Presence and resume across 3 WebSocket servers, with and without sticky routing.
// "shared" stands for Redis: a presence hash with heartbeat TTLs and a per-room log
// with sequence numbers (a Redis Stream). Clock is fake seconds; no real sockets.
function fnv1a(s) {
  let h = 0x811c9dc5;
  for (const c of s) { h ^= c.charCodeAt(0); h = Math.imul(h, 0x01000193) >>> 0; }
  return h;
}

function run(design, sticky, serverDies) {
  let pool = ["ws-a", "ws-b", "ws-c"];
  let rr = 0;
  const pick = (user) => sticky ? pool[fnv1a(user) % pool.length] : pool[rr++ % pool.length];
  const sessions = { "ws-a": {}, "ws-b": {}, "ws-c": {} }; // local design: buffers in server memory
  const log = [];                                          // shared design: one log in Redis
  const first = pick("alice");
  sessions[first].alice = [];
  for (let seq = 1; seq <= 5; seq++) {
    log.push(seq);
    if (seq > 3) sessions[first].alice.push(seq); // alice is gone; her server buffers 4 and 5
  }
  if (serverDies) { sessions[first] = {}; pool = pool.filter((s) => s !== first); }
  rr = 1; // the next round-robin pick is a different server
  const second = pick("alice");
  let result;
  if (design === "shared") result = "replays [" + log.filter((q) => q > 3).join(",") + "]";
  else if (sessions[second].alice) result = "replays [" + sessions[second].alice.join(",") + "]";
  else result = "no session here: missed 4,5";
  return first + " -> " + second + ": " + result;
}

console.log("Part 1: alice saw seq 1-3, messages 4 and 5 arrive while she is away, she reconnects");
for (const dies of [false, true]) {
  console.log(dies ? "  her server crashed:" : "  only her network dropped:");
  for (const [design, sticky] of [["local", false], ["local", true], ["shared", false]]) {
    const name = (design === "local" ? "buffer in server memory" : "log in shared store") + (sticky ? ", sticky" : ", round-robin");
    console.log("    " + name.padEnd(38) + run(design, sticky, dies));
  }
}

console.log("\nPart 2: presence with heartbeats every 10s and a 30s TTL in the shared store");
const TTL = 30;
const seen = { alice: 0, bob: 0 };
const online = (now) => Object.keys(seen).filter((u) => now - seen[u] < TTL).join(",") || "-";
for (let t = 0; t <= 60; t += 10) {
  if (t <= 20) seen.alice = t;          // alice's laptop sleeps after t=20: no close frame
  if (t !== 30) seen.bob = t;           // bob misses one heartbeat at t=30
  console.log("  t=" + String(t).padStart(2) + "s online: " + online(t));
}
console.log("  alice shows online until t=" + (20 + TTL - 1) + "s: a dead socket is only noticed by TTL");

console.log("\nPart 3: sticky by hash(user) mod N when ws-c is removed (1,200 users)");
const users = Array.from({ length: 1200 }, (_, i) => "user" + i);
const before = { "ws-a": 0, "ws-b": 0, "ws-c": 0 };
let survivorsMoved = 0, survivors = 0;
for (const u of users) {
  const was = ["ws-a", "ws-b", "ws-c"][fnv1a(u) % 3];
  const now = ["ws-a", "ws-b"][fnv1a(u) % 2];
  before[was]++;
  if (was !== "ws-c") { survivors++; if (now !== was) survivorsMoved++; }
}
console.log("  before: ws-a " + before["ws-a"] + ", ws-b " + before["ws-b"] + ", ws-c " + before["ws-c"]);
console.log("  hash mod N: " + survivorsMoved + " of " + survivors + " users on healthy servers are routed elsewhere");
console.log("  cookie stickiness: 0 of " + survivors + " move; only the " + before["ws-c"] + " ws-c users reconnect");

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 44 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

What is the difference between strong and eventual consistency?

Continue