Design a multiplayer trivia game
A live quiz where a host runs rounds and thousands of players answer on their phones at the same moment. The hard part is not the quiz screen: it is one synchronized game loop driven by the server, the same deadline for every player whatever their clock and latency, fair speed scoring, fan-out to 100,000 sockets, and players who reload or lose Wi-Fi in the middle of a question.
The question
Design a live trivia game like Kahoot!. A host starts a game on a big screen, players join from their phones with a PIN and a nickname, and then the host runs about 15 questions. Each question has a 20 second timer, faster correct answers score more, and a leaderboard shows between questions.
Most games have 30 players in a classroom, but we also run big live events with up to 100,000 players in one game. Walk me through the game loop, how everyone gets the same deadline, how answers are judged fairly, how players rejoin after a reload, and what the host and player screens show.
Real-life examples: Kahoot!, HQ Trivia (2017 to 2020), Wayground (formerly Quizizz), Jackbox Games party games, Slido quizzes, Mentimeter quizzes
Requirements: which trivia game are we building?
A live trivia game is a quiz that everybody plays at the same time. A host (a teacher, a presenter, or a studio show) controls the pace, and players answer on their own devices. After each question the game reveals the right answer, adds up points and shows who is winning.
Kahoot! is the classroom version. HQ Trivia was the TV-show version, with a live host on video and a cash prize; its record was about 2.38 million players at once, in March 2018, before the company shut down in 2020. Jackbox is the living-room version with a shared TV.
These products look alike but stress different parts of the system. A classroom game is 30 players on school Wi-Fi, while a live show is 100,000 players who all tap within the same few seconds. The first job in the interview is to find out which game we are building. Each answer below changes the design; the middle column is what this guide assumes.
| Question | Answer we assume | What it changes |
|---|---|---|
| Who controls the pace: a host, or each player? | A host. Everyone sees the same question at the same time | One shared game clock on the server, not a per-player quiz |
| How many players in one game? | Usually 5 to 50; big events up to 100,000 | Fan-out across many socket servers and aggregated answer counts |
| Does speed matter for the score? | Yes: correct answers score up to 1,000 points, more for faster | Fair timing becomes a scoring problem, not only a UX one |
| Is there a prize? | Sometimes (brand events). Never money in this version | Anti-cheat effort is moderate: no bank-grade proof |
| Is the host on a separate screen? | Yes: a laptop on a projector shows the question; phones show answer buttons | Two different clients with different payloads |
| Can people join late or rejoin after a reload? | Yes to both | Session tokens and a snapshot of the current state |
| Is there live video, like HQ Trivia? | No, the host screen is the stage. Video is a follow-up | No need to sync questions with a video stream |
| Which devices and networks? | Cheap Android phones on crowded venue Wi-Fi and 4G | Tiny payloads, few re-renders, reconnects that do not stampede |
Neighbouring guides already cover some pieces, and we link to them rather than repeat them. The online judge guide estimates the server clock offset (how far a device clock is from the server clock) from a few round trips, and the countdown timer guide turns a deadline into a display that does not drift. The live leaderboard guide covers ranking UI and sorted sets, the poll widget guide covers vote counting with sharded counters, and the live comments guide covers high fan-out chat. This guide ties them together: the synchronized game loop.
Functional requirements
- The host creates a game from a quiz and gets a short numeric PIN to show on screen.
- Players join with the PIN and a nickname, wait in a lobby, and can rejoin after a reload without losing their score.
- The host starts the game and moves through questions. Each question has 2 to 4 answers and a time limit (5 to 120 seconds, 20 by default).
- Players answer once per question. The answer is locked when they tap; they cannot change it.
- After the time limit (or when everyone has answered), the game reveals the right answer, the answer distribution, and each player's points.
- Between questions the host screen shows the top 5. Each phone shows the player's own score and rank.
- Late joiners can enter between questions and play the rest of the game.
Non-functional requirements
- Same deadline for everyone: every phone shows the question closing within about 100 ms of the same real moment, whatever its clock or latency.
- Fast phase changes: when the host taps Next, phones switch screens within 300 ms at the 95th percentile on 4G.
- Fair judging: the server decides whether an answer was in time and how fast it was. A slow network costs at most a small, bounded number of points.
- Scale: 100,000 sockets in one game, with up to 100,000 answers arriving in about 10 seconds (bursts near 20,000 per second).
- Small and cheap: a phase message to players under 300 bytes; the player app under 100 KB of JavaScript compressed so it loads fast on a crowded network.
- Resilient: a player who drops for 5 seconds comes back to the right screen, with the right time left and their answer still counted.
- Accessible: answers work by color, shape, and text; an extended time option; a screen reader hears the question and time without a stream of noise.
Out of scope
- The quiz editor (creating questions, uploading images): a normal form-and-CRUD problem.
- Live video of a presenter: the live streaming guide covers low-latency video. We mention how to sync with it later.
- Payments, prizes, and identity checks for cash games.
- Chat between players: the live comments guide covers that kind of fan-out.
Think first
A classroom game has 30 players. A live event has 100,000. Which parts of the design would you keep the same for both, and which would you only build for the big one?
Keep the same: the state machine, the message protocol, server-side timing, the snapshot for reconnects, and the player UI. A 30 player game is just a small instance of the same loop, and two code paths would drift apart.
Only for the big one: sharding sockets across many servers, aggregating answer counts in shards before the host sees them, jittered reconnects, and sending a summary leaderboard instead of the full list. These are scale tactics that sit behind the same protocol.
Check your understanding · Exercise 1
Which answers change the architecture, not only the UI? Select all that apply.
What makes a live trivia game hard
The first version everyone sketches is a quiz app: fetch the questions, show one, start a 20 second setInterval on the phone, post the answer, show the next one. It works for one player and breaks the moment a second player joins.
A live game is not 1,000 quizzes running side by side. It is one game that 1,000 screens are looking at. Five properties drive every decision in the rest of this guide.
- One shared clock. The question opens and closes at the same real moment for everyone. A phone whose clock is 4 seconds fast, or whose messages arrive 400 ms late, must still show the same deadline. A local
setIntervalstarted on message arrival gives each player a different deadline. - The server is the referee. Points depend on correctness and speed, and players have a motive to cheat. So the right answer, the deadline, and the response time all live on the server. The client is a remote control and a display, nothing more.
- Bursty fan-out. Traffic is not smooth. For 15 seconds nothing happens, then the host taps Next and one message must reach 100,000 sockets at once. Then 100,000 answers arrive in a 10 second burst, most of them in the first 3 seconds.
- Unreliable networks in one room. A classroom or a conference hall puts hundreds of phones on one Wi-Fi access point. Packets drop, sockets close, phones sleep when the screen turns off. Reconnects are normal, not an edge case, and they arrive as a crowd.
- Two very different screens. The host screen on a projector shows the question, images, a big timer, and a live chart of answers. A phone shows four colored buttons. Sending the host's payload to every phone wastes bandwidth and leaks information.
Put numbers on the burst to see why it matters. A 100,000 player game with 15 questions has 5 phase changes per question (question, answering, locked, reveal, scores), so 75 broadcasts, each reaching 100,000 sockets: 7.5 million socket writes in a 20 minute show. The answers are 1.5 million small writes, and roughly a third of each question's answers land in the first 2 seconds of the window: about 17,000 per second at peak.
| Moment in a question | Direction | Messages | Size each | What can go wrong |
|---|---|---|---|---|
| Host taps Next | Server to all players | 100,000 | about 150 bytes | Slow broadcast: far players see the question late |
| Answering window | Players to server | up to 100,000 in 20 s | about 80 bytes | Burst at the start; answers in flight at the deadline |
| Live answer count | Server to host only | 4 to 10 per second | about 100 bytes | Sending each answer to the host would flood it |
| Reveal and scores | Server to each player | 100,000 personal results | about 120 bytes | Personal messages cannot be one shared broadcast |
| Wi-Fi blip in the hall | Players to server | thousands of reconnects | a snapshot each | A reconnect storm that hits the servers at once |
The design that falls out of these properties has four pillars, and each gets its own chapter: a server-driven state machine that every screen renders; absolute deadlines in server time that each client converts with its clock offset; server-side judging with a clear rule for answers in flight; and a fan-out layer that broadcasts shared phases and sends small personal results. Everything else (the lobby, reconnects, leaderboards, accessibility) is built on those four.
Check your understanding · Exercise 2
A first draft starts a 20 second setInterval on each phone when the question message arrives, and the phone sends timeTaken with its answer. What goes wrong? Select all that apply.
The game loop is a state machine on the server
A game moves through a fixed set of phases: waiting in the lobby, showing a question, accepting answers, locked, revealing the answer, showing scores, then the next question or the end. At any moment the game is in exactly one phase, and only certain moves are allowed (you cannot reveal a question nobody has seen). That is the textbook shape of a state machine: a list of states, the events that move between them, and rules about which moves are legal.
The state machines lesson covers the idea in general. Here we decide where it runs, because that choice decides who can cheat, who can pause, and what happens when a laptop goes to sleep.
Decision Who owns the game state and decides when the phase changes?
What we pick: Run the state machine on the server. The cost is one round trip per phase change (tens of ms), and in return we get a single referee that holds secrets, owns time, and survives a host laptop going to sleep. Clients become views: they render whatever phase the server says and send intents, never conclusions.
seq and is broadcast to every screen.The phases and what triggers each move
| Phase | Host screen | Player phone | Leaves when |
|---|---|---|---|
lobby | PIN, join link, nicknames appearing | "You're in! See your name on screen?" | Host sends start |
question | Question text and image, a short reading countdown | "Get ready" and the question number | Server timer: reading time ends (about 3 to 5 s) |
answering | Answers, big timer, answer count | Four colored shape buttons | Server timer hits closesAt, or all players answered |
locked | "Time's up" | "Answer locked" or "Too late" | Server finishes judging (under 1 s) |
reveal | Right answer highlighted, bar chart of picks | Correct or wrong, points this round | Host sends next |
scores | Top 5 with movement | Your score and rank | Host sends next; goes to question or finished |
finished | Podium | Final rank | Game is archived after a few minutes |
Two kinds of events move the machine: host commands (start, next, skip, pause, end) and server timers (reading time over, answering window closed). Player actions never change the phase directly; they only add answers. The one exception is a convenience rule: when every connected player has answered, the server may close the window early, and that is still the server's decision, counted on the server.
The client as a view of the phase
Each phase message carries a sequence number that grows by one at every transition, so a client can drop messages that arrive out of order or twice (which happens around reconnects). The client keeps only the latest phase and renders a screen for it. There is no client-side game logic beyond "is this newer than what I have".
type Phase =
| { kind: "lobby"; players: number }
| { kind: "question"; index: number; total: number; answersAt: number }
| { kind: "answering"; index: number; choices: number; opensAt: number; closesAt: number }
| { kind: "locked"; index: number }
| { kind: "reveal"; index: number; correct: number; you: RoundResult }
| { kind: "scores"; index: number; you: { score: number; rank: number } }
| { kind: "finished"; you: { score: number; rank: number } };
type GameView = { seq: number; phase: Phase; myAnswer?: number };
function onPhase(view: GameView, msg: { seq: number; phase: Phase }): GameView {
if (msg.seq <= view.seq) return view; // stale or duplicate
const sameQuestion = "index" in msg.phase && "index" in view.phase
&& msg.phase.index === view.phase.index;
return { seq: msg.seq, phase: msg.phase, myAnswer: sameQuestion ? view.myAnswer : undefined };
}
Notice what is missing. The answering phase has choices: 4 but no answer texts and no correct index: the player phone shows shapes, the host projector shows the words. The correct field appears only in reveal. The times are absolute server times (opensAt, closesAt), not "20 seconds from now": the next chapter explains why.
Check your understanding · Exercise 3
The host's laptop loses Wi-Fi during the answering phase of question 7. With a server-side state machine, what happens? Select all that apply.
Joining with a PIN, rejoining after a reload, and late joiners
Joining must take seconds on a phone with no account: type a short numeric PIN shown on the projector, pick a nickname, done. Kahoot! made this flow famous. Behind its simplicity are three questions: how does the PIN find the right game, how does the server know you after a reload, and what happens to someone who arrives in the middle of question 5?
The PIN: short, live, and rate limited
A 6 digit PIN has 1,000,000 values. If 20,000 games are live at once, a random guess hits some game 2% of the time. That is fine for a classroom quiz, as long as three rules hold: PINs exist only while a game is live, the join endpoint is rate limited per IP and per device (for example 10 attempts a minute), and the host can lock the lobby once everyone is in.
The PIN maps to a game id and to the region or cluster that runs it, so the join call can route the player to the right socket servers. When a game ends, its PIN waits through a cool-off period before reuse, so an old QR code cannot drop someone into a stranger's game.
Who are you after a reload?
Phones reload pages all the time: the user switches to the camera, the browser drops the tab to save memory, the screen locks for 10 minutes. When the page comes back, the player must get their seat, their nickname, and their score back, not a fresh "enter PIN" screen.
Decision How does the server recognise a returning player?
What we pick: Use an opaque session token, stored per game, as the default. Offer accounts as an option for graded or prize games where identity matters. The token is a capability for one seat in one game, so it is low value if stolen, and it expires within hours.
Why localStorage and not sessionStorage? sessionStorage survives a reload but belongs to one tab, and scanning the QR code a second time opens a new tab with empty storage. A cookie would also work, and an HttpOnly cookie is safer against XSS, but if the socket servers live on a different site from the page it becomes a third-party cookie, which Safari blocks and other browsers restrict. Passing the token in the first socket message, not in the URL, keeps it out of server logs.
Late joiners
Someone scans the QR code during question 5. The server gives them a seat with 0 points and a snapshot of the current phase. What they see depends on the phase.
In answering, one fair choice is to let them answer if at least a few seconds remain, scored against the same opensAt as everyone else, so they lose speed points for arriving late. Many products simply say "You'll join at the next question", which is simpler and avoids a player who joined with 1 second left. We take the simple rule: late joiners wait for the next question phase, and the leaderboard ranks them normally.
- Duplicate nicknames: the server appends a number ("Sam 2") rather than rejecting, because typing on a phone is slow.
- Rude nicknames: a server-side filter, plus a host button to kick a player. Kicking revokes the token so a reload cannot bring them back.
- Capacity: the server rejects joins above the game's limit with a clear message, not a spinner.
- Lobby list: the host screen shows names as they join, but at 5,000 players it shows a count and the latest 20 names. Rendering 5,000 name chips is both slow and unreadable.
Check your understanding · Exercise 4
Which choices make rejoining after a reload safe and smooth? Select all that apply.
Synchronized rounds: one deadline for every screen
The host taps Next and the server broadcasts "question 4 is open, 20 seconds". Ana in the front row is on fast Wi-Fi and gets the message 40 ms later. Ben is on a congested 4G cell and gets it 600 ms later. Chen's phone clock is 3 seconds fast because it has not synced with the network in weeks.
If each phone starts a 20 second countdown on arrival, Ben's timer ends 560 ms after Ana's. Nobody notices on screen, but it matters for fairness and for the moment the reveal appears: Ben's phone still shows half a second left when the projector says "Time's up".
Decision How does the server tell clients when the answering window closes?
What we pick: Send absolute server times and correct them with a clock offset. The online judge guide builds the offset estimate (several pings, keep the sample with the lowest round trip time), and the countdown timer guide renders a deadline without drift. We reuse both here and add one trick of our own: schedule phases slightly in the future.
Schedule the open slightly in the future
Even with perfect offsets, a broadcast to 100,000 sockets takes time to fan out: the first socket gets it in 30 ms, the last in maybe 800 ms. If opensAt is "now", early receivers see 20.0 s and late ones see 19.2 s. The fix is a lead time: the server sends opensAt = now + 1000 ms, so every phone receives the message before the window opens, shows "Get ready", and flips to the buttons at the same instant.
The reading time of the question phase gives us that buffer for free. Its message already carries answersAt, the moment answering will open. The server broadcasts answering about a second before that moment, with opensAt equal to answersAt, so the fan-out delay hides inside the reading time.
// One ping sample: t0 and t1 are performance.now() when the ping left and
// the pong arrived; serverTime is the server's epoch ms inside the pong.
let best = { rtt: Infinity, serverMs: 0, perfMs: 0 };
export function addSample(t0: number, t1: number, serverTime: number) {
const rtt = t1 - t0;
// Assume the server read its clock halfway through; keep the shortest trip.
if (rtt < best.rtt) best = { rtt, serverMs: serverTime + rtt / 2, perfMs: t1 };
}
export function resetClock() { // on reconnect and after the page wakes up
best = { rtt: Infinity, serverMs: 0, perfMs: 0 };
}
export function serverNow(): number {
return best.serverMs + (performance.now() - best.perfMs);
}
export function msLeft(closesAt: number): number {
return Math.max(0, closesAt - serverNow());
}
performance.now() is monotonic, so a wall clock change on the phone does not move the deadlinePinning the offset to performance.now() instead of Date.now() matters on phones: if the OS adjusts the wall clock in the middle of a game (a network time sync), a Date.now() based countdown jumps, while the monotonic clock does not. After the page has been hidden or the phone slept, re-sync: some browsers pause performance.now() while the device sleeps, and a cheap ping on visibilitychange costs nothing.
Pre-sending the next question without leaking the answer
The host projector needs the question text and image at the instant of question. Downloading a 300 KB image at that moment adds a visible delay on venue Wi-Fi, so the host client prefetches the next question's media during the scores phase. That is safe because the host is trusted and the media holds no answer. Players are the risk: anything sent to a phone before the reveal is readable by a curious player with DevTools.
- Never send the correct answer early. The
correctindex travels only inreveal. Pre-sending it "encrypted" with the key at reveal time works, but the gain is tiny: the reveal message is already under 200 bytes. - Question text to phones is optional. Kahoot!-style phones show only shapes, so there is nothing to leak. Formats that show text on the phone (needed for accessibility and for remote players) send it in the
questionphase, which is fine: everybody sees it at the same time anyway. - Encrypt-ahead pays off for big payloads in huge games. If a show sends a 50 KB image to 100,000 phones, sending it encrypted 30 s early and broadcasting a 32 byte key at
opensAtshows the image the instant the question opens, without giving anyone a head start. The client decrypts withcrypto.subtle.decrypt. This is a niche optimization; mention it, do not lead with it. - Syncing with live video is a separate problem. In a show like HQ Trivia, a host reads the questions on live video, which reaches viewers several seconds late, and the in-app question has to line up with the host's voice. The live streaming guide covers stream latency; a common approach is to delay the data to match the video for each viewer rather than the other way round.
Check your understanding · Exercise 5
A phone's clock is 3 s fast and its messages arrive 400 ms late. The server sends opensAt 1 s in the future and closesAt 20 s after that. With a correct offset, what does the player see? Pick one.
Speed scoring, fairness, and leaderboards
Points reward both being right and being fast. Kahoot! publishes its formula: a correct answer scores round(1000 * (1 - (responseTime / timeLimit) / 2)). Answer instantly and you get close to 1,000 (Kahoot! gives the full 1,000 to correct answers under 0.5 s); answer at the last moment and you still get 500.
We adopt the same shape. Kahoot! still shows answer streaks, but its help center says streaks no longer add points, so we keep a streak only for display. The real question is what responseTime means, and who measures it.
Do the arithmetic first. With a 20 s limit, every 100 ms of response time costs 1000 * 0.1 / 20 / 2 = 2.5 points. A player whose answers take 300 ms longer to reach the server loses 7.5 points per question, or about 110 points over 15 questions. That sounds small, but near the top of a 10,000 player game, places can easily be separated by less than that.
With a 5 s limit the penalty quadruples to 10 points per 100 ms. Latency is a scoring bias, not just a UX detail.
Decision Which clock decides whether an answer is in time, and how fast it was?
What we pick: Use the third rule: server receipt time minus a capped latency allowance, plus a grace period of the same size for answers in flight. A cheater who games their pings wins at most 250 ms, worth about 6 points on a 20 s question. For cash prizes, drop the allowance to 0 and accept the bias in exchange for simplicity you can defend.
Try the rules in the lab. Ana, Ben and Chen each tap 3 s after their buttons appear, and Dev taps 0.1 s before the end. Under the device clock, Chen's fast clock shifts his whole window 3 s early; under strict receipt, Dev's last-moment tap arrives late. With the capped rule, Ben stays within a few points of fair until his one-way latency is well past the 250 ms cap.
Ana: 925 pts, fair 925; Ben: 918 pts, fair 925; Chen: 924 pts, fair 925; Dev: late, fair 503
The grace period and the lock
At closesAt some answers are still on the wire. If the server locks the question at exactly closesAt, a player who tapped at 19.95 s on screen sees "Too late". So the server keeps accepting until closesAt + grace, then moves to locked, then judges. The grace equals the latency cap (250 ms), and the reveal waits for it: a quarter of a second nobody notices on the projector.
const CAP_MS = 250;
function judge(q: OpenQuestion, a: IncomingAnswer, conn: Conn): Verdict {
if (q.answers.has(a.playerId)) return q.answers.get(a.playerId)!; // first answer wins
const allowance = Math.min(conn.medianRttMs / 2, CAP_MS);
const effectiveAt = a.receivedAt - allowance;
if (effectiveAt > q.closesAt || a.receivedAt > q.closesAt + CAP_MS) {
return { status: "late" };
}
const t = Math.max(0, effectiveAt - q.opensAt);
const correct = a.choice === q.correct;
const points = correct ? Math.round(1000 * (1 - t / q.limitMs / 2)) : 0;
const verdict = { status: "accepted" as const, correct, points, t };
q.answers.set(a.playerId, verdict);
return verdict;
}
The verdict is stored, not sent. The phone gets an immediate acknowledgement (answer.ack with status: accepted) so it can show "Answer locked in", but whether it was correct and how many points it earned wait for reveal. Sending correct: true in the ack would leak the answer: a group of friends could have one person tap each shape and share which one came back correct.
Edge cases worth naming
- Retries: the phone retries an answer whose ack did not come back within 1.5 s. The server's "first answer wins" rule makes retries safe. If the first copy arrived, the retry is ignored; if it was lost, the retry's own receipt time counts, so a lost packet costs speed points, never a double score.
- Double taps: the button disables on the first tap, and the server ignores a second answer anyway.
- Answer while disconnected: the phone queues the tap and sends it on reconnect. The server judges it on receipt like any other answer, so it counts only if it arrives before
closesAtplus the grace period; otherwise "Too late". The phone does not send a local tap time, for the cheating reason above. - Everyone answered early: the server closes the window as soon as all connected players answered. It counts only players connected in the last few seconds, or one player with a dead socket would hold up the room.
- Ties: two players with equal points are ordered by total response time, then by join time. Write the rule down; players will ask.
Check your understanding · Exercise 6
Your server uses "receipt time minus min(RTT/2, 250 ms)". Which statements are true? Select all that apply.
Leaderboards between rounds
Scores feed the leaderboard. In the scores phase after each reveal, the host screen shows the top 5 with arrows for who moved up, and every phone shows something like "You're 14th with 4,410 points". The top 5 is easy: one sorted list, five rows. The personal rank is the expensive part at scale, and it is also what keeps the 99,995 players outside the top 5 interested.
The live leaderboard guide covers the ranking UI, ties, and sorted-set mechanics in depth, so we do not repeat them. Here we only decide what rank to show each player between rounds.
Decision What rank does each phone show in a 100,000 player game?
What we pick: Exact ranks for small games (under 5,000 players the lookups are trivial), and the hybrid for large ones. A score histogram with 100 buckets, built once per question, answers every player's band, and the batched lookup only runs for the top 1,000.
Anti-cheat basics
Most cheating in a classroom quiz is mischief, not fraud: bots flooding a lobby with 500 fake names, someone joining as a rude nickname, a friend group sharing answers in a chat. The design already handles the dangerous part, because the phone never has the correct answer before reveal and the server measures time. What is left is a short list of cheap defenses.
| Threat | Defense | Where |
|---|---|---|
| Reading answers from the page or network | Correct index sent only in reveal; acks say "accepted", not "correct" | Protocol |
| Forged speed | Server receipt time, capped latency credit | Gateway judging |
| PIN guessing | Join rate limit per IP and device, PINs recycled only after a cool-off | Join API |
| Bot floods in the lobby | Join rate limits, host lock, kick revokes the token, a challenge (such as a CAPTCHA) only when joins spike | Join API and host UI |
| Message spam on the socket | Per-socket limit (for example 20 messages a second), one answer per question | Gateway |
| Rude nicknames | Server-side filter, optional generated nicknames, host kick | Join API and host UI |
| Friends sharing answers | Cannot be stopped in a shared room. For remote prize games, shuffle choice order per player and flag accounts with identical answer patterns | Product policy |
Check your understanding · Exercise 7
A player opens DevTools during answering. What can they learn with this design? Pick one.
Fan-out at scale: 100,000 phones, one question
A classroom game fits on one server with room to spare. A 100,000 player event does not, and the shape of its traffic is unusual: long quiet gaps, then one message to everybody, then a flood of tiny messages from everybody. This chapter is mostly server work, but a frontend engineer must be able to sketch it, because it decides what the client protocol looks like and how reconnects behave.
Decision Which transport carries phases down and answers up?
What we pick: WebSocket, with an SSE plus POST fallback for networks that block it. The choosing realtime lesson walks through this trade-off in general; for a game the bidirectional, low-overhead channel and the server-measured RTT tip it clearly. Libraries such as Socket.IO ship a similar fallback (HTTP long polling) out of the box.
Gateways, a game coordinator, and pub/sub
Split the work in two. Gateways are socket servers, and each holds a share of the connections. We plan for about 25,000 per node, so 100,000 players need 4 nodes, and we run 8 for headroom and rolling deploys. That is a conservative planning number: a tuned server can hold far more idle sockets, but a broadcast burst needs CPU to spare.
The game coordinator owns one game's state machine and timers: exactly one coordinator per game, chosen by hashing the game id. It publishes each phase change once to a pub/sub channel such as game:4821, and every gateway with players in that game subscribes and writes the message to its local sockets. One publish becomes 8 deliveries becomes 100,000 socket writes, in parallel.
Answers up: judge near the edge, count in shards
Sending 100,000 answers to one coordinator in a burst of 17,000 a second is possible, but there is a simpler shape. When the window opens, the coordinator gives each gateway the question's opensAt, closesAt, and correct index (servers are trusted). Each gateway judges its own players' answers with the same judge function, keeps a small sharded counter (one count per choice, per gateway), and every 250 ms sends the coordinator a batch: the counts plus the verdicts.
The coordinator sums 8 small counters instead of 100,000 events, the same sharded counter idea the poll widget guide uses for vote totals. It still applies "first answer wins" when it merges verdicts, because a player who reconnects may land on a different gateway and send the answer again.
| Message | Who gets it | How it is sent | Why |
|---|---|---|---|
Phase change (question, answering, locked) | Everyone | One publish, fan-out by gateways | Identical for all, so build it once |
| Live answer count | Host only | Coordinator to host, 4 times a second | Players do not need it; it would leak the trend |
reveal with your result | Each player | Gateway adds the player's own verdict to the shared part | Shared part built once; personal part is about 40 bytes |
scores with your rank | Each player | Gateway looks up ranks in one batched call to the score store | 100,000 single lookups would be a stampede |
Ranks are the expensive part. A score store (a sorted set, as in the live leaderboard guide) holds every player's total, and for the scores phase each gateway asks it once for the ranks of all its players in a single batched call. In large games most phones get a percentile band instead of an exact rank, as decided in the scoring chapter, which makes this call cheaper still.
Reconnect storms
The venue's Wi-Fi controller restarts. 3,000 phones lose their sockets in the same second, and if each retries immediately, a gateway gets 3,000 handshakes plus 3,000 snapshot requests in 100 ms, often right at a phase change.
The client side of the fix is in the retries and backoff lesson: exponential backoff with full jitter (first retry at a random time between 0 and 1 s, then 0 to 2 s, then 0 to 4 s, capped at 10 s). The server side: snapshots come from the gateway's in-memory copy of the current phase, never from the database, and a gateway that is overloaded answers with a retryAfter hint instead of accepting connections it cannot serve.
Check your understanding · Exercise 8
100,000 players, 8 gateways. Which choices keep the reveal step fast? Select all that apply.
The host screen and the player screen
The two clients look like one product but have opposite needs. The host screen runs on a laptop connected to a projector: big type readable from the back of a hall, the question and image, a large countdown, a live bar of how many have answered, music, and the podium at the end. The player screen runs on a 4 year old Android phone held in one hand: four big buttons, a status line, and its own score. One is a stage, the other is a remote control.
Decision One web app with a role switch, or two apps?
What we pick: Two entry points sharing a protocol package. The phone is the constrained client and the one that must load on bad Wi-Fi in a hall, so it deserves its own budget. The host app can afford richer media because it runs on a laptop on a cable or good Wi-Fi.
Payloads: what each screen gets
| Phase | Host receives | Player receives |
|---|---|---|
question | Text, image URL, answer texts, time limit (about 1 KB) | Index, total, answersAt (about 80 bytes) |
answering | opensAt, closesAt, then answer counts 4 times a second | opensAt, closesAt, number of choices (about 90 bytes) |
reveal | Correct index, distribution per choice | Correct index, your verdict and points (about 120 bytes) |
scores | Top 5 with names, scores, and movement | Your total and rank (about 60 bytes) |
Keeping the player payload to shapes and indices does three things. It keeps every broadcast small (100,000 copies of 90 bytes is 9 MB; of 1 KB it would be 100 MB). It forces players to look up at the shared screen, which is the social point of the game. And it means there is nothing to inspect: no answer text, no correct index, no distribution until the reveal.
Players who need the text on their phone (low vision, a seat behind a pillar, remote players) can switch on a "show question on my device" mode, covered in the accessibility chapter.
Host screen details that matter
- Live answer count updates 4 times a second from the coordinator, not once per answer. The bar animates with a CSS transform so the projector laptop does not re-layout 4 times a second.
- Audio needs a user gesture before it can play. The host's Start click is that gesture: create and resume the
AudioContextthere, and keep it for the whole game. - Keep the screens awake: the Screen Wake Lock API (
navigator.wakeLock.request("screen")) stops the projector laptop and the phones from dimming mid-game. Re-acquire it onvisibilitychange, because the lock is released when the page is hidden. - Fullscreen via
element.requestFullscreen()from the Start click, with the timer still readable if the host stays windowed. - Pause and skip are host commands like any other: the server stops the timer, records how much time was left, and broadcasts a new
closesAton resume.
Think first
A teacher runs the game from a laptop but wants to control it from their phone while walking around the room. How does the design change?
Very little, because the host is already just a client that sends commands. The phone becomes a second host client: it authenticates as the game's owner, receives the host payloads it needs (current question, answer count), and sends next and pause.
Two host clients can both tap Next. Each command carries the seq it was issued against, and the server ignores a next whose seq is no longer current, so a double tap cannot skip a question.
Check your understanding · Exercise 9
Why does the player phone receive only shapes and indices during answering? Select all that apply.
Reconnecting mid-game: resume from a snapshot
Ben's phone drops off Wi-Fi at 12 s into question 6 and switches to 4G. The socket is dead, but the browser may not notice for a long time: a TCP connection can look open for tens of seconds after the network under it vanished. When it reconnects 4 seconds later, the game has moved on: the window may have closed, the reveal may be showing. Ben must land on the right screen, with the right time left, with his answer (if he tapped one) counted.
Noticing quickly that the socket is dead
- Application heartbeat: browsers do not expose WebSocket ping frames to JavaScript, so the client sends a tiny
pingmessage every 5 s and expects apongwithin 3 s. Two misses and it reconnects. The same pings feed the RTT used for fairness. - Browser hints: the
offlineevent, andvisibilitychangeback to visible, trigger an immediate check instead of waiting for the next heartbeat. - Page lifecycle: close the socket on
pagehideand reconnect onpageshow. In some browsers an open socket keeps a page out of the back/forward cache, and in every browser a page restored from that cache has a stale view of the game.
resuming is a separate state because the screen must not accept taps until the snapshot says which phase it is in.Decision How does a reconnecting client catch up?
What we pick: Snapshot. The game is a state machine, and the only thing a view needs is the current state. Because times in the phase are absolute server times, the snapshot does not go stale on the way: a phone that receives it 300 ms late simply shows 300 ms less time left.
type Snapshot = {
type: "snapshot";
seq: number;
serverTime: number; // also refreshes the clock offset
phase: Phase; // includes opensAt / closesAt when answering
me: {
playerId: string;
nickname: string;
score: number;
rank: number | null;
answer?: { index: number; choice: number; status: "accepted" | "late" };
};
};
resume. It is just the current phase plus what is personalThe client treats a snapshot like a phase message that always wins (it replaces the view even if seq matches, because the personal part may be new). If the phone had a tap queued while offline, it sends it right after the snapshot if the snapshot still says answering for the same question; the server's grace rule decides. If the snapshot already shows reveal, the queued tap is dropped and the phone shows "You were offline when time ran out" instead of a generic "Wrong".
Lobby. Step the server, toggle the player's connection, and tap an answer.
Try it in the lab: step the server through a question, disconnect the player during answering, tap an answer while offline, then advance the server and reconnect at different moments. The player's screen never shows a phase the server did not reach, and the queued answer only counts when the reconnect lands inside the window.
Check your understanding · Exercise 10
A player reconnects 6 s after dropping. In that time the server moved from answering to locked to reveal. What should the phone do? Select all that apply.
Architecture, data model, and interface
With the big decisions made, draw the client. Both apps share the same skeleton; the player app is the one we detail, because it runs on the weakest device and the worst network. The rule behind every boundary: network code knows nothing about React, and components know nothing about sockets. In between sits one small store that holds the server's latest word.
| Piece | Responsibility | Why it is its own piece |
|---|---|---|
| Join page | PIN and nickname form, calls POST /join, stores the token | Plain HTTP and HTML, works before any socket exists |
| Socket client | Opens, heartbeats, reconnects with jitter, sends resume, queues outgoing messages | All connection state in one place; testable without a UI |
| Clock sync | Ping samples, offset, serverNow() | Shared by the countdown, the answer queue, and the debug panel |
| Game store | Latest seq, phase, my answer, my score; applies phases and snapshots | One reducer that accepts only newer state; components subscribe to slices |
| Answer outbox | Sends the tap, waits for ack, retries, keeps it across a reconnect | Answers must survive a dropped socket without double sending |
| Phase screens | One component per phase: Lobby, GetReady, Buttons, Locked, Result, Rank, Final | A switch on phase.kind; each screen is small and dumb |
| Countdown | Renders msLeft(closesAt) once a second, aligned to the second | Reuses the countdown timer guide's ticker; owns the live region timing |
| Wake lock and lifecycle | Wake lock, visibilitychange, pagehide, re-sync triggers | Browser lifecycle quirks in one module |
Data flow for one question
Follow question 4 end to end. The host taps Next, and the host app sends next over its socket. The coordinator checks the seq, moves the machine to question, and publishes one message on the game's channel. Each gateway trims it to the player shape and writes it to its sockets.
On the phone, the socket client parses it and hands it to the store, the store accepts it because its seq is newer, and only the phase screen component re-renders. When the player taps, the outbox sends the answer up, the gateway judges and acks, and the store marks the answer as locked. On reveal the same path runs downward again, with the gateway adding the personal result.
Two details keep this cheap on a slow phone. First, the store is outside React (a tiny external store read with useSyncExternalStore), so a heartbeat pong or an RTT update does not re-render anything. Second, the countdown subscribes to time, not to the store: it re-renders once a second, while the phase screen re-renders only on the 5 phase changes and the player's own tap and ack, about 7 times per question.
Think first
Why not put the game state in a server-state library like TanStack Query and refetch on each socket message?
Those libraries shine for request and response data that can be stale and refetched. Here the server pushes the full new state, ordered by seq, and there is nothing to fetch: refetching would add a round trip to every phase change and 100,000 GET requests per broadcast.
A small reducer over pushed messages is the better fit. Server-state libraries still suit the parts that are normal requests: the join call, the final results page, and the host's quiz list.
Check your understanding · Exercise 11
Which boundaries in the player app are worth keeping? Select all that apply.
Data model: what lives where
The server holds the real game: the quiz with correct answers, every player, every answer, every score. The phone holds a thin, disposable view of it. The test for each field on the client is simple: if the phone forgot it right now, could the next snapshot restore it? If yes, it is a cache of the server and lives in memory; only the session token must survive a reload on its own.
| Data | Where it lives | Lifetime | Why there |
|---|---|---|---|
| Session token, player id, game id | localStorage, keyed by game id | Until the game ends, or 6 hours | Must survive reloads and closed tabs |
Current phase and seq | Game store (memory) | This page load | Replaced by every phase message or snapshot |
| My answer for this question | Game store and answer outbox | Until the next question | Outbox keeps it across a reconnect |
| Score, rank | Game store | This page load | Comes in reveal, scores, and snapshots |
| Clock offset and RTT samples | Clock module (memory) | This connection | Re-measured on connect and wake |
| Nickname, theme, extended time setting | localStorage | Across games | Convenience; not secret |
| Correct answers, other players' answers | Server only | Game lifetime, then archived | Secret until reveal; not needed on the phone |
type Game = {
id: string; pin: string; quizId: string; hostId: string;
seq: number; phase: Phase; locked: boolean;
questionIndex: number; createdAt: number;
};
type Question = {
index: number; text: string; imageUrl?: string;
choices: string[]; correct: number; limitMs: number;
};
type Player = {
id: string; gameId: string; nickname: string; tokenHash: string;
score: number; streak: number; joinedAt: number; kicked: boolean;
};
type Answer = {
gameId: string; questionIndex: number; playerId: string;
choice: number; receivedAt: number; responseMs: number;
correct: boolean; points: number;
};
Question.correct before reveal, nor any other player's AnswerNotice tokenHash: the server stores a hash of the session token, as it would for a password reset link, so a leaked database dump does not let anyone take over seats in a live game. The Answer key is (gameId, questionIndex, playerId), which is what makes "first answer wins" a simple insert-if-absent and makes retries harmless.
Check your understanding · Exercise 12
Which data should the player phone persist in localStorage? Select all that apply.
Interface: the HTTP calls and the socket protocol
The protocol is small on purpose. Two HTTP calls get a player into a game; everything after that is socket messages with a type field. Plain JSON is fine for these sizes: a phase message is around 100 bytes, and per-message compression (permessage-deflate) costs server memory per connection that is not worth spending on tiny frames at 100,000 sockets.
POST /api/games/join
Content-Type: application/json
{ "pin": "482193", "nickname": "Priya" }
200 OK
{ "gameId": "g_4821", "playerId": "p_77f2", "token": "k9Q...32 bytes...",
"nickname": "Priya", "socketUrl": "wss://rt-eu3.example.com/play" }
404 { "error": "game_not_found" } // wrong PIN or game ended
409 { "error": "lobby_locked" } // host locked the game
429 { "error": "rate_limited", "retryAfter": 30 }
// client to server
type ClientMsg =
| { type: "resume"; token: string; lastSeq: number }
| { type: "answer"; questionIndex: number; choice: number }
| { type: "ping"; id: number }
| { type: "host.command"; command: "start" | "next" | "skip" | "pause" | "end"; atSeq: number };
// server to client
type ServerMsg =
| Snapshot
| { type: "phase"; seq: number; phase: Phase }
| { type: "answer.ack"; questionIndex: number; status: "accepted" | "late" | "duplicate" }
| { type: "pong"; id: number; serverTime: number }
| { type: "host.counts"; questionIndex: number; answered: number; byChoice: number[] }
| { type: "error"; code: "kicked" | "game_over" | "overloaded"; retryAfter?: number };
seq when it changes the game; acks and pongs do notThe socket messages are a union on type, shared as one TypeScript package by the gateway, the player app and the host app, so a renamed field fails the build instead of a live game. The client ignores any type it does not know, which lets the server add messages (an emoji reaction, a host announcement) before every phone has reloaded.
- Auth on the socket: the first message must be
resumewith the token; the gateway closes the socket if it does not arrive within 5 s. The host's socket uses a signed-in session instead of a seat token. - Answers carry the question index, so an answer that lands after the next question opened cannot be counted for the wrong question.
- Host commands carry
atSeq, so two taps on Next from two host devices move the game once. - Errors are typed:
kickedclears the stored token and shows a message,game_overshows the final screen,overloadedmakes the client back off forretryAfterseconds. - Versioning: a
vquery parameter on the socket URL lets the gateway reject clients that are too old after a protocol change, with a "please reload" message.
Check your understanding · Exercise 13
Why does an answer message carry questionIndex even though the server knows the current question? Select all that apply.
Accessibility and performance on cheap phones
A timed, colorful, fast game is close to a worst case for accessibility. Time pressure hurts players who read slowly, use a switch device, or rely on a screen reader. Color-coded buttons fail color-blind players, and a countdown that announces every second makes a screen reader unusable.
None of these needs a separate app; each needs a deliberate choice. The visual accessibility and screen readers lessons cover the general techniques, and this chapter applies them to the game. The second half of the chapter is about the other constraint every player feels: cheap phones on crowded Wi-Fi.
Time limits and WCAG 2.2.1
WCAG success criterion 2.2.1, Timing Adjustable, asks that users can turn off, adjust, or extend time limits. It has a real-time exception, but only when the limit is a required part of a live event (the W3C example is an auction) and no alternative to the limit is possible. It also has an essential exception, for limits whose removal would invalidate the activity.
A live quiz can argue for one of these, but the argument is weak when alternatives clearly exist, and it misses the point: a student with dyslexia in a graded classroom quiz should not lose because of a 20 second timer. Offer real accommodations.
- Extended time per player: the host (or the player, in non-competitive games) turns on 1.5x or 2x time. The server gives that player their own
closesAt, scores them against their own limit, and holds the lock until the longest window ends. The host screen shows "waiting for 2 players". - Longer timers for the whole game: a host setting, cheap and fair for everyone.
- A self-paced mode: the same quiz as an assignment with no shared clock, for anyone who cannot play live.
- Pause: the host can pause any question; the server stores the time left and sends a new
closesAton resume.
Kahoot! is a useful reference here. Its accessibility settings let hosts show questions and answers on player devices, which it requires for screen reader support, and offer an unlimited time option only inside its accuracy mode, which drops speed from scoring. That pairing is the honest one: when a player's time limit changes, their speed score must change with it, which is why our extended time option scores each player against their own limit.
Color, shape, and text on every answer
Kahoot!'s buttons pair each color with a shape: a red triangle, a blue diamond, a yellow circle, a green square. That is the right instinct, because WCAG 1.4.1, Use of Color, says color must not be the only way to tell things apart, and red and green are exactly the pair many color-blind players confuse.
We go one step further and give each button a visible letter (A to D) and an accessible name such as "A, triangle" or, in text mode, "A, Paris". The projector shows the same letter and shape next to each answer text, so the mapping is explicit. Buttons fill a quarter of the screen each, far above the 24 by 24 CSS pixel minimum of WCAG 2.5.8, Target Size.
Screen readers: announce sparingly
- Text on the device: a player using a screen reader cannot read the projector. "Show question on my device" mode sends the question and answer texts to that phone in the
questionphase (the same moment everyone sees them, so no unfairness). - Focus on phase change: when the answering screen appears, move focus to the question heading, so the reader starts there and the user can tab to the answers. Do not move focus again until the next phase.
- One polite live region for the game status: "Question 4 of 15", "Answer locked", "Correct, 812 points", "You're 14th". Each is announced once.
- Time announcements at milestones only: 10 seconds left and 5 seconds left, through the same polite region. Never every second. The visible countdown has
aria-hiddendigits plus a "time left" button that reads the current value on demand. - Respect reduced motion: with
prefers-reduced-motion: reduce, the podium and leaderboard reorder without sliding, and the timer ring does not pulse.
const MILESTONES = [10, 5];
function Countdown({ closesAt, announce }: Props) {
const secs = Math.ceil(useMsLeft(closesAt) / 1000);
const spoken = useRef(new Set<number>());
useEffect(() => {
if (MILESTONES.includes(secs) && !spoken.current.has(secs)) {
spoken.current.add(secs);
announce(`${secs} seconds left`);
}
}, [secs, announce]);
return (
<div className="timer">
<span aria-hidden="true">{secs}</span>
<button type="button" onClick={() => announce(`${secs} seconds left`)}>Time left</button>
</div>
);
}
Check your understanding · Exercise 14
Which choices make the answering screen usable with a screen reader? Select all that apply.
Performance on cheap phones and crowded Wi-Fi
Picture the worst realistic room: 400 students in a lecture hall, mostly on 3 to 5 year old Android phones, all on two Wi-Fi access points, with some on 4G in a basement. The game must load in a few seconds, react to a tap instantly, and keep working through packet loss. The runtime performance lesson covers main thread work in general; the levers that matter here are bundle size, render count, tap feedback, and network behaviour.
| Budget | Target | How |
|---|---|---|
| Player JavaScript | Under 100 KB compressed | Separate entry point; no chart, media, or editor code; small UI library |
| Join page interactive | Under 3 s on a mid-range phone over 4G | Server-rendered HTML form that works before JavaScript loads |
| Tap to visual feedback | Under 100 ms (good INP is 200 ms or less) | Mark the button pressed and show "Answer sent" before the ack |
| Renders per question | About 7 screen renders (5 phases, tap, ack) plus 1 per second for the timer | External store with slice subscriptions; timer isolated |
| Bytes per question to a phone | Under 1 KB | Shapes and indices only; no images on phones |
| Reconnect to correct screen | Under 2 s after the network returns | Heartbeat detection, jittered retry, snapshot from gateway memory |
Tap feedback deserves a word. The ack takes a round trip, often 100 to 400 ms on venue Wi-Fi, and if the button waits for it, the tap feels broken and players tap again. So the tap updates the UI at once (the chosen shape grows, others fade, the status reads "Answer sent"), the outbox sends the message, and the ack only upgrades the status to "Locked in". If the ack never comes, the outbox retries; the UI does not undo the tap, because the server's first-answer rule makes the retry safe.
A short vibration with navigator.vibrate adds feel on Chromium-based Android browsers. Safari and current Firefox do not support the Vibration API, so treat it as a bonus and never as the only feedback.
- Cache the shell: a service worker caches the player app's shell, so the second game in the same classroom loads from disk. The join page still needs the network for the PIN check.
- Fonts and images: system fonts on the phone; the shapes are inline SVG, not image files.
- No layout work at 60 fps: the timer ring animates with a CSS transform and the digits use
font-variant-numeric: tabular-nums, so a second ticking over does not shift the layout. - Small frames: plain JSON for 100 byte messages; batch the host's answer counts at 4 per second instead of one per answer.
- Observability: each phone reports a few numbers per game (RTT median, clock offset, reconnect count, tap to ack time) and the gateways report broadcast fan-out time at p50 and p95. A bad venue shows up as a cluster of high RTT and reconnects from one game, which is the evidence you need before blaming the code.
Check your understanding · Exercise 15
Players in a hall say "my taps don't register" although the server shows their answers. What is the most likely cause and fix? Select all that apply.
Running it in the interview
Forty-five minutes is not enough to cover every chapter, so choose the spine and let the interviewer pull you into the branches. The spine is the game loop: a state machine on the server, phases broadcast with absolute server times, answers judged on the server. If you get those three right in the first 20 minutes, the rest are follow-ups you can answer from a solid base.
| Minutes | What to do | What to draw |
|---|---|---|
| 0 to 5 | Ask the scale and scoring questions; say "one shared clock, server is the referee" | Nothing yet, or a list of requirements with numbers |
| 5 to 12 | The state machine: phases, who triggers each move, client as a view | The phase diagram with host commands and server timers |
| 12 to 20 | Synchronized deadline and fair judging: offset, lead time, receipt time, grace | A timeline with two players at different latencies |
| 20 to 28 | Client architecture and the socket protocol | Socket client, store, outbox, phase screens; message types |
| 28 to 36 | Scale: gateways, pub/sub, sharded counts, personal results; reconnect storms | The fan-out diagram |
| 36 to 45 | Reconnect snapshot, accessibility, cheap phones; whatever the interviewer picks | The connection state machine if asked |
Say the numbers out loud. "With a 20 second limit, each 100 ms of latency costs 2.5 points" shows you understand why fairness is a scoring problem. "100,000 copies of a 90 byte message is 9 MB per broadcast" shows why the player payload is tiny. Interviewers remember candidates who quantify.
Follow-up questions to expect
Think first
"We want a live presenter on video, like HQ Trivia. How do you keep the question in sync with the host's voice?"
The video reaches each viewer several seconds late, and that delay differs per viewer. Showing the question at server time T would put it on screen before the presenter reads it. So the client measures its own video delay (from timestamps embedded in the stream) and delays the data phases by the same amount.
The deadline must then be per-player too: each player's window is shifted by their video delay, and the server judges against that shifted window, capped so nobody can claim a 30 second delay. Lower-latency streaming (see the live streaming guide) shrinks the problem but never removes it.
Think first
"Players are in Europe and Australia in the same game. Is that fair?"
Sockets terminate at gateways near each player, but the game coordinator lives in one region, so answers from far away travel further. The capped latency credit removes up to 250 ms of the gap. Beyond that, judging at the edge helps: each regional gateway stamps receipt time on its own synchronized clock, so the long hop to the coordinator does not count.
Be honest about limits: a player on a 900 ms satellite link still pays something. For prize games, publish the rule.
Think first
"The server running the game coordinator crashes in the middle of question 9. What happens?"
Keep the coordinator's state small and write it to a fast store on every transition: phase, seq, times, scores. Another node takes over the game (the hash ring moves it), reloads the state, and continues. Answers received by gateways in the meantime are buffered at the gateways, which are a separate failure domain.
Clients barely notice: their sockets are on gateways, not on the coordinator. At worst the reveal is a second late. If the window was open, the new coordinator keeps the original closesAt, so the deadline does not move.
Check your understanding · Exercise 16
You have 5 minutes left and have not covered reconnects. What is the strongest short answer? Select all that apply.
Summary
- The game is a state machine on the server. Host commands and server timers move it; clients render the phase they are told and send intents, never conclusions.
- Send absolute server times (
opensAt,closesAt), convert them with a clock offset pinned toperformance.now(), and schedule openings slightly in the future so every phone flips at once. - The server judges: receipt time minus a capped, server-measured latency credit, a short grace period, and first answer wins. With a 20 s limit, 100 ms is worth 2.5 points.
- Never put the correct answer on a phone before reveal. Acks say accepted, not correct.
- Fan out shared phases once through pub/sub to socket gateways; personalize results at the gateway; count answers in shards.
- Reconnect with heartbeat detection, full-jitter backoff, and a snapshot of now. A session token in storage brings back the seat.
- Keep the player app tiny and dumb: shapes and indices, instant tap feedback, few renders, under 100 KB of JavaScript.
- Accessibility is a design choice: extended time per player, color plus shape plus text, text on device, milestone-only time announcements.
Check your answer
Tick what your own design covered. Pick the level you're interviewing for: each level includes the ones before it.
References
- How points work (Kahoot! Help Center) The published speed scoring formula this guide adopts.
- RFC 6455: The WebSocket Protocol Framing, ping and pong control frames, and closing.
- WebSockets API (MDN)
- Performance: now() method (MDN) The monotonic clock that the offset is pinned to.
- Screen Wake Lock API (MDN)
- Exponential Backoff And Jitter (AWS Architecture Blog) Why full jitter spreads reconnect storms.
- Understanding SC 2.2.1: Timing Adjustable (W3C)
- Understanding SC 1.4.1: Use of Color (W3C)
- Understanding SC 2.5.8: Target Size (Minimum) (W3C)
- Accessible options for playing Kahoot! (Kahoot! Trust Center) Questions on player devices for screen readers, and unlimited time only in accuracy mode.
- Interaction to Next Paint (web.dev)
Concepts behind it
- WebSocketsOne long-lived connection, opened by an HTTP handshake, on which browser and server can each send a message at any moment. The usual pick for chat, multiplayer, and collaboration, and the one where you own the protocol, reconnects, and scaling.
- State machines for UIModel a flow as a fixed set of states and the events that move between them, so impossible combinations like "loading and failed at once" can't happen and every button does the right thing in every state.
- Retries, timeouts, and backoffHow a client survives a flaky network and a struggling server: give every request a deadline, retry only the failures that can heal, space the retries out with exponential backoff and jitter, and stop retrying when the server or the network is clearly down.
- Choosing a real-time techniqueHow to pick between short polling, long polling, server-sent events, WebSockets, and WebRTC for each feature, starting from who talks, how often, and how fresh the data must be, then checking scale, proxies, battery, and the fallback you will ship.
- Screen readers and live regionsHow screen readers turn a page into speech or braille, how every control gets its accessible name and description, and how to announce changes like search results, saves, and errors with live regions.
- Color, contrast, and motionHow people with low vision, color blindness, motion sensitivity, or custom system themes see your UI, and the rules that keep it usable: contrast ratios, never color alone, reflow at 400% zoom, target size, and the user preference media queries.
- Runtime performanceKeeping the page responsive after it loads: finding long tasks on the main thread, splitting or moving that work, and avoiding needless re-renders so every click and keypress paints in under 200 ms.