Skip to the solution
MediumSystem Design13 min read

When would you use WebSockets vs polling vs SSE?

712 views

The problem

Question presented to candidate: "Our app needs to show fresh data without the user pressing refresh. You could poll, long-poll, use Server-Sent Events or open a WebSocket. How do they differ, and how would you pick one for, say, a live sports score page versus a chat app?"

What a strong answer should cover:

  • How each works: short polling (ask on a timer), long polling (server holds the request until there is news), SSE (one HTTP response that never ends, server to client only), WebSocket (an upgraded connection carrying messages both ways)
  • The cost model: polling trades latency against empty requests; push transports trade request load for long-lived connections that the server and every proxy must hold
  • SSE details that matter: plain HTTP, built-in reconnect with Last-Event-ID, text only, the 6-connection-per-domain limit over HTTP/1.1 that HTTP/2 removes
  • WebSocket details: HTTP Upgrade handshake (RFC 6455), tiny frame overhead, binary support, no built-in reconnect or resume, idle timeouts at load balancers
  • A decision rule tied to the product: update frequency, direction of data, client count, and infrastructure (proxies, serverless, mobile battery)

Clarifying questions expected:

  • How often does the data change, and how stale may the screen be?
  • Does the client need to send frequent messages back, or only receive?
  • How many clients are connected at once, and what sits between them and the servers (CDN, corporate proxies, a serverless platform)?

Code / implementation expected: Optional โ€” a clear comparison and a reasoned choice; the example simulates all four on the same minute of events.

  • realtime
  • websockets

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 ยท 10 sections

Read the code

Polling, long polling, SSE and WebSocket on one simulated minute: requests, latency, bytes
// Polling vs long polling vs SSE vs WebSocket on one simulated minute.
// Times are integer milliseconds on a fake clock; the network is a fixed delay.
const EVENTS = [3200, 3900, 17000, 41500, 41530, 58100]; // when the server learns news
const END = 60000;
const ONE_WAY = 50;          // ms, client <-> server, one direction
const HTTP_HEADERS = 700;    // assumed bytes of request + response headers per HTTP exchange
const PAYLOAD = 60;          // bytes of JSON per event

function shortPoll(interval) {
  let requests = 0, empty = 0, i = 0;
  const lat = [];
  for (let t = interval; t <= END; t += interval) {   // request reaches server at t
    requests++;
    let got = 0;
    while (i < EVENTS.length && EVENTS[i] <= t) { lat.push(t + ONE_WAY - EVENTS[i]); i++; got++; }
    if (got === 0) empty++;
  }
  return { requests, empty, lat, bytes: requests * HTTP_HEADERS + lat.length * PAYLOAD };
}

function longPoll(timeout) {
  let requests = 0, empty = 0, i = 0, pendingAt = ONE_WAY; // first request arrives at 50 ms
  const lat = [];
  while (pendingAt < END) {
    requests++;
    const next = i < EVENTS.length ? Math.max(EVENTS[i], pendingAt) : Infinity;
    let answerAt;
    if (next <= pendingAt + timeout && next < END) {
      answerAt = next;
      while (i < EVENTS.length && EVENTS[i] <= answerAt) { lat.push(answerAt + ONE_WAY - EVENTS[i]); i++; }
    } else {
      answerAt = Math.min(pendingAt + timeout, END);
      empty++;
    }
    pendingAt = answerAt + 2 * ONE_WAY; // response travels back, client sends a new request
  }
  return { requests, empty, lat, bytes: requests * HTTP_HEADERS + lat.length * PAYLOAD };
}

function stream(kind) {
  // One HTTP request (SSE) or one upgrade (WebSocket), then every event is pushed.
  const lat = EVENTS.map(() => ONE_WAY);
  let framing = 0;
  EVENTS.forEach((_, n) => {
    if (kind === "sse") framing += ("id: " + (n + 1) + "\n").length + "data: ".length + "\n\n".length;
    else framing += 2; // RFC 6455: 2-byte header for an unmasked server frame under 126 bytes
  });
  return { requests: 1, empty: 0, lat, bytes: HTTP_HEADERS + framing + EVENTS.length * PAYLOAD };
}

const pad = (s, n) => String(s).padEnd(n);
const avg = (a) => Math.round(a.reduce((x, y) => x + y, 0) / a.length);
console.log("Part 1: one client, one minute, 6 events, one-way delay 50 ms");
console.log(pad("strategy", 22) + pad("requests", 10) + pad("empty", 7) + pad("avg ms", 8) + pad("max ms", 8) + "bytes");
const rows = [
  ["short poll every 5s", shortPoll(5000)],
  ["short poll every 1s", shortPoll(1000)],
  ["long poll (30s hold)", longPoll(30000)],
  ["SSE", stream("sse")],
  ["WebSocket", stream("ws")],
];
for (const [name, r] of rows) {
  console.log(pad(name, 22) + pad(r.requests, 10) + pad(r.empty, 7) + pad(avg(r.lat), 8) + pad(Math.max(...r.lat), 8) + r.bytes);
}
const lp = longPoll(30000).lat;
console.log("long poll latencies: " + lp.join(", ") + " (the 41530 event arrived while no request was waiting)");

console.log("\nPart 2: the same choice for 1,000,000 connected clients");
const N = 1000000;
for (const s of [5, 1]) {
  const rps = N / s;
  console.log("short poll every " + s + "s: " + rps.toLocaleString("en-US") + " requests/s, " +
    ((rps * HTTP_HEADERS * 8) / 1e9).toFixed(1) + " Gbit/s of headers alone");
}
const lpPerMin = longPoll(30000).requests;
const lpRps = Math.round((N * lpPerMin) / 60);
console.log("long poll (" + lpPerMin + " requests/min each, as above): " + lpRps.toLocaleString("en-US") + " requests/s, plus 1,000,000 held requests");
console.log("SSE or WebSocket: 1,000,000 open connections, 0 requests/s when idle");

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 24 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

When do you use a read replica vs a cache?

Continue