HardProblem 30 of 36 in the system design series
Design Online Chess: Real-Time Moves, Fair Clocks and Matchmaking

An online chess platform pairs two people of similar strength, relays their moves to each other as they happen, keeps a ticking clock for each side, and records the finished game along with updated ratings. This practice design targets a busy chess site with millions of games a day across bullet, blitz and longer time controls.
Interviewers reach for this problem because it is a compact tour of real-time systems. It touches persistent connections, authoritative server state, race conditions in matchmaking and graceful recovery when a phone drops off Wi-Fi mid-game. The data volume is small, so candidates cannot hide behind storage discussions.
What makes it tricky is that latency and fairness are both measured in milliseconds. In a one-minute game, a slow relay or a clock that trusts the client can decide the result. The design has to keep the server as the sole referee while still feeling instant to both players.
This is an interview practice design for a Online Chess-like service, built from first principles. It is not a description of how Online Chess is actually built.
Functional Requirements
A player chooses a time control such as 5+3 blitz and asks for a game; the system finds an opponent with a comparable rating. Once paired, each move a player makes should appear on the other board immediately. Both sides have clocks, with an optional increment added after each move, and running out of time loses the game.
When the game ends, both ratings are recalculated with a system like Elo or Glicko, and the complete move list is saved for later review. Spectating, tournaments and anti-cheat analysis are excluded. Each is valuable, but they build on top of the core loop of matching, playing and recording, which is where the interview focus belongs.
Find a match
Pair the player with someone of a similar rating.
by ratingby time control
Play moves live
Moves appear on the opponent's board instantly.
real-time
Game clocks
Each side has a timer; run out and you lose.
e.g. 5 + 3 blitz
Ratings & history
Update ratings after a game and save every move.
Elo / Glicko
- Out of scope today: spectators, tournaments and cheat detection
Non-Functional Requirements
The latency target is under 100 ms for a move to reach the opponent, because in bullet games a tenth of a second is a meaningful slice of the clock. Fairness is equally strict: only the server decides whether a move is legal and how much time remains, since a tampered client could otherwise lie about either.
Games must survive disruption. A player's network dropping or one of our machines failing should never lose the position or the clocks. Matchmaking should feel snappy too, with most players seated in a game within a few seconds. Notice that throughput is not on this list; the constraints are about responsiveness and trust.
Low move latency
A move should reach the opponent almost instantly.
< 100 ms
Correct & fair
Server decides legal moves and whose clock is running.
server is the judge
Durable games
A disconnect or a server crash never loses a game.
no lost moves
Quick matching
Most players should find a game within seconds.
< 5 s typical
Scale Estimation
Assume 10 million games per day, which is about 116 new games every second. At around 80 moves per game, the system handles roughly 9,000 moves per second on average and perhaps 30,000 at peak. If a typical game lasts ten minutes, about 70,000 games are in progress at any moment, which means around 140,000 open player connections.
Storage is tiny by comparison. A completed game with metadata and the move list is roughly 1 KB, so 10 million games produce about 10 GB a day and around 3.6 TB a year. The takeaway is that this is not a big-data problem. The challenge is managing many long-lived connections and routing small messages with very low latency.
10million
Games per day
≈ 116 new games / sec
9K avg
Moves per second
80 moves / game · ≈ 30K peak
70K avg
Live games at once
116 / sec × 10 min
3.6TB
Storage per year
1 KB / game · 10 GB / day
Small data, many tiny messages → connections and latency are the challenge
Core Entities & API
Four entities model the domain: Player, Match request representing someone waiting in the queue, Game, and Move. Keeping the match request separate from the game makes it easy to cancel a search without touching game records.
POST /matchmaking with a time_control returns a ticket_id while the server searches. During play, both clients hold a WebSocket on /games/{game_id}; the client sends moves like e2e4 with a move number, and the server pushes moves, clock values and the final result. After the game, GET /games/{game_id} returns the full move list, result and rating changes. WebSockets are the natural fit because both sides need to push messages at unpredictable moments.
Core entities
PlayerMatch requestGameMove
API
- POST /matchmaking { time_control }→ { ticket_id }
- WS /games/{game_id} { move: "e2e4", move_no }→ move, clocks, game over
- GET /games/{game_id}→ { moves, result, ratings }
High-Level Design
Clients connect through a WebSocket gateway whose sole job is maintaining one persistent connection per player and forwarding messages. When someone requests a game, the matchmaking service places them in a waiting pool stored in Redis, organised by time control and ordered by rating. Once it finds a suitable pair, it creates the game on a specific game server.
From that point, every move for that game is routed to the same game server. It validates each move, runs the clocks, and after every move writes the current board and clock state to Redis, so the authoritative state never lives only in one process's memory.
Each game event is also published to Kafka. One consumer persists completed games to the game database for history and analysis, while another, the rating service, reads results and adjusts both players' ratings. This keeps slow work like rating calculations off the latency-critical move path.
Deep Dive 1 — Matchmaking
The simplest strategy pairs the next two people in line regardless of rating. Matches are instant, but a newcomer might face a master, which is miserable for both. The opposite extreme, requiring nearly identical ratings, produces balanced games but leaves players at unusual ratings waiting indefinitely, especially during quiet hours.
A widening rating band balances the two. The search starts narrow, say within 50 points, and expands every few seconds the player keeps waiting. Busy periods produce tight matches quickly, while quiet periods still find someone eventually. In Redis, each time control gets a sorted set whose score is rating, so finding candidates inside a band is a single range query.
The hazard is concurrency: two matchmaker instances could each select the same waiting player. Removing both players inside one atomic Lua script prevents that; if either has already been taken, the script fails and the matcher simply searches again. Because pools are split by time control and region, they shard cleanly across Redis nodes.
Pair whoever waits
- + Instant matches
- + Very simple
- − Beginner vs grandmaster
Exact rating match
- + Very fair games
- − Long waits
- − Rare ratings never match
Widening rating bandChosen
- + Fair and fast
- + Adapts to traffic
- − Needs atomic pairing
- One Redis sorted set per time control: score = rating
- Lua script pops both players atomically → no double match
- Shard pools by time control and region
Deep Dive 2 — Moves & Clocks
Every move carries a sequence number alongside the notation, which lets the server discard duplicates caused by retries and reject moves that arrive out of order. On receipt, the game server checks that it is this player's turn and that the move is legal in the current position. The client's own validation is only for user experience; the server never trusts it.
Clock accounting also happens on the server, using its own timestamps. It subtracts the time elapsed since the previous move from the mover's clock and then adds the increment. A small lag compensation allowance prevents players on slower connections from being penalised for network delay they cannot control.
After updating, the server writes the board and both clocks to Redis and pushes the new position and remaining times to both players. One subtle point: a player who never moves again must still lose on time. So the server schedules a timer for each game that fires at the exact moment the active clock would reach zero, ending the game without waiting for input.
- 1Send the moveOver WebSocket, with the move number
- 2Validate on serverRight turn? Legal move? Expected move number?
- 3Update the clockServer time minus elapsed, plus increment
- 4Save stateBoard and clocks written to Redis
- 5Push to both playersNew position and both clock values
- Server timer per game fires on flag fall, even if no move arrives
Deep Dive 3 — Reconnects & Failures
Each game is owned by exactly one server, chosen by consistent hashing on the game ID. Both players' connections and all moves for that game converge on the owner, which avoids coordinating state between machines on every move.
Dropped connections are routine on mobile. When a client returns, it sends the game ID and the last move number it saw, and the server replies with only the missing moves and current clocks. Meanwhile the absent player's clock continues to run, and if they stay away beyond a configured limit, the game is scored as abandoned.
If a game server dies, the hash ring assigns its games to another server, which reads the most recent position and clock values out of Redis while both clients re-establish their sockets and carry on. Kafka holds every move as a durable log, so even if Redis loses data, the full game can be reconstructed and saved. The result is that no single failure erases a game in progress.
One owner per game
Route by game ID, so both players and every move hit the same server.
Resume after a disconnect
Client reconnects with game ID and last move number; server sends what it missed.
Abandon timer
Your clock keeps running; stay away too long and the game is lost.
Game server crash
Another server loads board and clocks from Redis and takes over.
Durable move log
Every move goes to Kafka, so a finished game can always be rebuilt.
Trade-offs
Widening the rating band faster shortens queues but produces more lopsided pairings; the expansion rate is a tunable dial. WebSockets deliver instant updates in both directions, but stateful connections complicate deploys and load balancing compared with stateless polling.
Making the server authoritative guarantees fair play at the cost of extra CPU for validating every move and running every clock. Splitting live state in Redis from history in a database keeps the hot path fast, though it introduces a sync step that must be reliable. The guiding principles are that the server referees, game state lives outside any single process, and moves are logged durably.
Match speed vs match fairness
A wider band means faster games, but less even opponents.
WebSocket vs polling
Instant two-way updates, but stateful connections that are harder to scale.
Server-authoritative
Fair and cheat-proof, but the server does more work for every move.
Redis for live, database for history
Fast live state, but we must keep the two stores in sync.
Server is the judge. State lives outside the server. Moves are never lost.
Key takeaways
- Assume about 70K live games and 9K moves per second; data is small, latency and connections are hard.
- Match with a widening rating band in Redis sorted sets and pair both players in one atomic Lua script.
- The server validates every move and owns the clocks, including a timer that ends games on flag fall.
- Persist board and clocks to Redis after each move so another server can take over after a crash.
- Log every move to Kafka so finished games and ratings never depend on a single store.
Interview questions
How does online chess matchmaking pair players by rating?
Waiting players sit in a sorted set per time control, scored by rating. The matcher searches for opponents within a narrow band, such as 50 points, and widens that band the longer someone waits. Because sorted sets support fast range queries, finding candidates is cheap. Both players are then removed atomically so neither can be paired into two games.
How do you prevent a player from being matched into two games?
Make the pairing step atomic. A Lua script running inside Redis removes both chosen players from the waiting pool in one operation and succeeds only if both were still present. If another matcher already took one of them, the script fails and the matcher searches again. This avoids locks while ruling out duplicate matches.
Who decides when a chess player runs out of time online?
The server does. It records timestamps itself rather than trusting client clocks, deducts elapsed time and adds increments after each move, and applies a small lag allowance. It also schedules a timer per game that fires when the active player's clock hits zero, so the game ends on time even if that player never sends another move.
What happens to a live chess game if the server crashes?
Board and clock state are written to Redis after every move, so they do not depend on the crashed process. Consistent hashing reassigns the game to another server, which loads the latest state, and both clients reconnect and resume. Every move is also logged to Kafka, allowing the complete game to be rebuilt and stored even if Redis loses data.
Why use WebSockets instead of polling for online chess?
Either player can act at any moment, and moves must arrive within about 100 ms. WebSockets keep a persistent two-way connection, so the server pushes moves and clock updates instantly without repeated requests. Polling either adds delay or floods the server with empty requests. The cost is managing stateful connections at the gateway layer.
Keep practising
- Learn the building blocks in the free system design course.
- Rehearse answers with system design interview questions.
- See which open jobs fit your skills: upload your resume.