← Back
Home › Systems › Consumer applications

chess-platform

Lets people find opponents, play timed chess games in real time, get rated, analyse, train and compete in tournaments, while screening for engine cheating.

Lichess, Chess.com

Open the chess-platform pagechess-platform Actor modelconcurrency: Isolated units of state that interact only by queued messages and handle one at a time: no locks on state, and the same code runs local or remote.›Compact state encodingdata-structures: Pack a game position into a few fixed bytes (24 for chess: occupancy bitboard plus nibbles) for training data and dedupeHdr histogramdata-structures: Log-linear bucketed histograms with bounded relative error for latency percentilesDeterministic simulationtestingdistributed: Run the whole system in one thread under a seeded scheduler with simulated network, disk and clock, so every failure replays from its seed.›Primary-backup replicationdistributed: One node orders all writes and ships them to standbys; the acknowledgement rule (none, one, all) sets what a crash loses, and an external arbiter decides who takes over.›Pub/sub fan-outdistributed: Deliver one published message to every subscriber of a topic by sharing one encoded payload and giving each connection a bounded queue with an explicit slow-consumer policy.›Lag-compensated turn timer witha refilling quotadistributed: A server-side turn timer that credits back each move's measured network lag, capped per move and by a quota that refills a fixed amount per move.›Glicko-2game-networking: Rating plus rating deviation (RD) and volatility per player, so new or inactive players move fast and established ones move slowly.›Swiss-system pairinggame-networking: Each round pairs competitors with equal scores, never twice and with balanced sides; a bye covers odd counts, and about log2(N) rounds expose a winner.›Process supervisionhardware-os: A parent process starts services, reaps their exits and restarts them under a delay and burst-limit policy, then stops them with SIGTERM before SIGKILL.›epoll event loopnetworking: One thread waits on thousands of non-blocking sockets with epoll_wait and handles only ready ones; the core of nginx, Redis and tokio.›WebSocketsnetworking: Full-duplex message framing over one upgraded HTTP connection; the standard way to push real-time data to browsers.›Pairing queueoptimization: Pair waiting players into fair matches by rating and latency, widening tolerances the longer each ticket waitsCoverage-guided fuzzingsecurity: Mutate inputs, run an instrumented target, and keep any input that reaches a new code edge; feedback turns blind guessing into incremental search.›Mutual TLSsecurity: TLS where the client also proves a certificate key, so every connection carries a verified, rotatable machine identity.›LSM-treestorage: Buffer writes in memory, flush them as immutable sorted files and merge in the background; turns random writes into sequential I/O.›Point-in-time recoverystorage: Periodic base backups plus a continuously archived change log let you rebuild the state as of any instant, then branch from there.›Graceful drainingsystems: On a termination signal, stop taking new work, finish in-flight work until a deadline, tell clients to reconnect elsewhere, then exit; clients resume from a token.›Hierarchical timing wheelsystems: Timers bucketed by expiry into wheels of slots, coarser wheels for far deadlines: O(1) insert and cancel for millions of timeouts.›Token bucketsystems: A bucket of up to b tokens refilled at rate r; requests spend tokens, enforcing an average rate while allowing bursts of b.›MPMC bounded queueconcurrency: Vyukov's array queue for many producers and consumers: each slot carries a sequence number, so one CAS per operation claims a slot without locks.›Work-stealing dequeconcurrency: Per-worker deque: the owner pushes and pops at the bottom without contention, idle workers steal from the top; the core of Rayon, Tokio and Go.›Cooperative schedulingconcurrency: Green threads/coroutines yielding at await points on a user-space schedulerFault injectiondistributed: Inject latency, errors, kills and partitions via proxies, netem or hooksRecord replaydistributed: Record every nondeterministic input (time, randomness, I/O, model and tool outputs, per-tick player input) and re-execute deterministically for debugging, replays and state rebuildLeasesdistributed: A lock that expires on its own: bounded-time ownership that survives crashes and partitions, given bounded clock drift and fencing tokens on writes.›Health checkingsystems: Decide per backend whether to route to it using probes and real-traffic errors, with consecutive-count hysteresis, capped ejection and a panic fallback.›SPSC ring bufferdata-structures: Bounded wait-free queue for exactly one producer and one consumer thread, built from an array and two indices with acquire/release ordering.›Topic matching triedata-structures: Trie over topic levels with + and # wildcards for subscription matchingBackpressuresystems: Bounded queues and credit-based flow so slow consumers throttle producersMinimum weight perfect matchinggraphs: Edmonds blossom algorithm and sparse blossom for matching syndromes and graphsPRNGs (PCG, xoshiro)numerics: Small, fast, statistically strong non-cryptographic generators: PCG32 and xoshiro256**, with seeding, parallel streams and unbiased ranges.›+ 11 moreEpoch-based reclamationconcurrency: Threads announce a global epoch while touching shared lock-free data; garbage retired in epoch e is freed once the epoch reaches e + 2.›Memory orderingconcurrency: What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.›Transparent interceptionnetworking: Redirect traffic to a local proxy (iptables TPROXY/REDIRECT, eBPF) without app changesSyscall interceptionsecurity: Intercept syscalls via ptrace, seccomp user notify or a user-space kernelFalse sharingconcurrency: Independent variables written by different cores on one cache line force the line to bounce between cores; pad or regroup hot data to fix it.›Fencing tokensdistributed: A number that grows with every lock grant and is checked by the protected resource, so a paused ex-holder's late write is rejected.›BFS and DFSgraphs: The two basic graph traversals; BFS gives hop distances and levels, DFS gives discovery/finish order for cycles, components and topological order.›Union-findgraphs: Disjoint-set forest with union by size and path compression; merges sets and answers same-set queries in effectively constant time.›+ 6 moreEbpfhardware-os: Verified in-kernel programs on hooks (kprobes, XDP, tc) with mapsPtrace process controlhardware-os: Ptrace: attach, read and write registers and memory, single-step, intercept signalsSeccomp bpfsecurity: Syscall filtering with seccomp BPF programsCPU caches and the memoryhierarchyhardware-os: Sizes, line widths and latencies of L1/L2/L3/DRAM on modern x86 and Apple M-series, and the data-layout rules that follow from them.›Sparse matrices (CSR)numerics: Compressed sparse row stores only nonzeros as row pointers, column indices and values; SpMV streams them once, so it is memory-bandwidth bound.›+ 3 more

How it is built

Architecture overview

An online chess platform has one hard real-time core and a wide, ordinary web application around it. The core is the live game: two clients send moves to a server that is the only authority on legality, the clocks and the result, and the server pushes each move to the opponent and to spectators within tens of milliseconds. Everything else is slower and can fail without ending a game: matchmaking (lobby, seeks, pairing waves), ratings, tournaments, puzzles, the archive of finished games, computer analysis and the fair-play pipeline that decides whether a player was helped by an engine.

The idea that makes it work is a strict split by latency class. A persistent-connection tier (WebSockets) only moves messages and holds no game logic; a game process per live game serialises that game's events and owns its clock; durable state lives in a database; and heavy work such as engine analysis is delegated to a pool of workers over a queue. Lichess is the open reference: its server lila is AGPL-3.0 open source, keeps pure chess rules in a separate library (scalachess), serves WebSockets from a separate process (lila-ws) connected to the main server through Redis, and farms Stockfish analysis out to volunteers' machines (fishnet) (lila README).

Parts:

 browser/app ◄──WebSocket──► socket tier ◄──pub/sub (Redis in lila)──► app server ──► DB (games, users)
                                  │                                        │   │
                                  │                                game actor  └─► rating / playban / puzzle logic
                                  │                         (rules, clock, result)
                                  │                                        │
 lobby offers ─► pairing wave ────┴──► start game ─► live game ─► finished ─┼─► archive ─► search, public export
                                                                            ├─► analysis queue ─► engine workers (fishnet)
                                                                            └─► fair-play models ─► moderator queue

How the parts fit together

Connection tier

The socket tier is a separate process from the application server. In Lichess, lila-ws terminates WebSocket connections and talks to the main server over Redis, its README showing the chain lila <-> redis <-> lila-ws <-> websocket <-> client. The reason for the split: connections are many and mostly idle and want an epoll event loop and many file descriptors, while application servers want CPU and are restarted often. Restarting the socket tier means a mass reconnect, which clients handle with jitter; Graceful draining tells them to reconnect elsewhere before the process exits.

WebSockets rather than long polling or Server-Sent Events because a game needs the client to send moves up and the server to push moves down on one connection with low overhead. Pub/sub fan-out is the delivery model: each game, tournament or lobby is a topic, spectators subscribe, and a message is encoded once and queued per connection with a bounded queue so one slow phone cannot block the room. The contract between tier and application: the socket tier validates framing and rate limits per connection, authenticates the session, and forwards only messages tagged with a user and a topic; it never interprets chess. The generic design is in Realtime pub/sub.

Game server

For each live game the server keeps a small state machine: the position and move list, whose turn it is, the clock state, pending draw offers and takebacks, and the connection status of both players. The Actor model fits: one actor per game handles messages (move, resign, flag, disconnect, timer fired) one at a time, so no locks are needed, and games on one machine are independent. Lichess keeps a round module with an asynchronous actor per game, and a move goes through the same path every time: validate, update the clock, check game end, persist, publish.

Move validation is done on the server with exact rules in a library; the client may also check, but only the server's verdict counts. Chess rules are detailed (castling, en passant, promotion, repetition, fifty-move rule), so use a tested rules library and check it against perft counts.

Clocks. The server owns time. A move reaches it after network delay, so charging the full elapsed time would punish players on slow connections, while crediting whatever the client claims would let anyone invent lag. Lag-compensated turn timer is the standard answer: each move's measured lag is credited back, up to a per-move cap and a quota that refills by a fixed amount per move. Lichess's scalachess library implements this in LagTracker: the compensation is the lag clipped to the current quota, and the quota is then raised by a gain and reduced by what was used, capped at a maximum. The flag timer (a player's time reaching zero) must fire promptly for thousands of games, one pending deadline per game: a Hierarchical timing wheel gives O(1) insert and cancel for that many timers; on every move the old deadline is cancelled and a new one is set.

Time controls are bucketed into speed classes, each with its own rating. Lichess classes games by estimated duration = initial seconds + 40 × increment, so 5+3 counts as 420 seconds (Lichess FAQ).

Matchmaking

There are two entry points. Open offers go in a lobby others can browse and accept; Lichess's lobby module stores them (Hook, Seek in its source). Pools are one-click quick pairing: a pool collects members and runs a wave at a fixed interval, then finds the best matching among everyone waiting. In lila the pool actor schedules a wave at a configured interval plus up to a second of jitter, and also runs one when enough players have gathered. Pairing is a weighted matching: a pair scores the rating difference minus bonuses for time waited, compatible rating ranges and similar behaviour records; a pair that is the same user, outside either player's rating range or blocked is forbidden; and the score must stay under a ceiling that depends on the lower rating (MatchMaking.scala).

This is pairing-queue (no page yet): pair by quality over a batch, widen tolerance with waiting time. Batching adds up to one wave of latency but beats greedy pairing of whoever arrives first. Two details are worth copying: players flagged as engine users or rating manipulators are paired among themselves (the same file), keeping them away from honest players; and behaviour records (abandoning games) enter the score.

The contract with the game server: matchmaking hands over (white, black, time control, rated flag), and the game server creates the game and notifies both sockets. A player must not be in two pairings at once, so pool and game start share a per-user "currently playing" lock.

Ratings

Each player has a rating per speed class (and per variant). Glicko-2 keeps a rating, a rating deviation (RD) and a volatility, so a new or returning player moves quickly and an established one slowly; plain Elo rating has only the rating and a fixed step. Lichess uses Glicko-2: new accounts start at 1500 with a deviation of 1000, a rating is marked provisional (a question mark) when the player has not played enough recent rated games, and leaderboard entry requires at least 30 rated games in the category, a game in the last week, a deviation below 75 in standard chess, and a top-10 rank (Lichess FAQ). The rating update runs once per finished rated game, in the same transaction as the result, so a game cannot be counted twice.

Tournaments

Two formats cover most events. Arena tournaments run for a fixed time; a player who finishes a game returns to the waiting list and is paired quickly with someone near their rank, so fast players play more games. Lichess scores a win 2, a draw 1, a loss 0, doubles points on a two-win streak and ranks by points with performance as the tie-break (Lichess arena FAQ). Swiss tournaments pair by score each round with no repeated pairings and balanced colours (Swiss-system pairing). Arena pairing is continuous matchmaking inside one event; Swiss is a batch problem per round. The contract: the tournament module asks for pairs, the game server creates the games, and results flow back to standings and ratings.

Fair play

The rules are plain: no engine, opening book, tablebase or advice during a real-time game, and no programmatic move input outside the official board and bot APIs (Lichess fair-play page). The server cannot see the player's screen, so detection is statistical and sits next to the game. This page covers defence only.

  • Signals from the game: the moves themselves (agreement with engine choices over many games, via stored analysis), the time spent per move, and account behaviour. Lichess has modules for two statistical models, irwin and kaladin, which report suspicion for an account; thresholds are settings, and a report above a threshold is either marked or sent to moderators (IrwinApi).
  • Human review: automated scores prioritise a moderator queue; sanctions are applied by a person or by a high-confidence rule.
  • Matchmaking and ratings: marked accounts are paired separately, and after a mark, opponents' losses are refunded: on Lichess, one minute after a mark, the cheater's 40 latest rated games in the last 5 days are examined and opponents with non-provisional ratings get a capped refund, never above 150 points (Lichess FAQ).
  • Abuse limits: Token bucket limits per user and per IP; the analysis request path has per-day and concurrency limits (FishnetLimiter in lila). A behaviour tracker lowers the standing of players who abandon games, which feeds matchmaking.

The fair-play system (no page yet) holds the pipeline details.

Analysis and puzzles

Computer analysis is batch work. After a game the server enqueues a job; Stockfish workers poll for jobs, run a fixed-effort search on each position and return evaluations, from which the server derives accuracy and move-quality labels. In Lichess, volunteers run the fishnet client with a personal key and lila-fishnet is the proxy between server and clients. Queueing makes analysis elastic: when the queue grows, analysis slows but games are unaffected. A Neural chess engine is an alternative worker if GPUs are available; positions can be stored in a fixed-size encoding (compact-state-encoding, no page yet) for the evaluation cache.

Puzzles are positions with a known best line that the engine has checked. A common design (assumed here, not read from the Lichess source) gives puzzles their own rating and serves ones near the user's, so each attempt moves both ratings. Public exports of games, puzzles and evaluations are provided by Lichess as open data (database.lichess.org).

Accounts, storage and operations

Auth service handles sign-in, sessions and API tokens for bots and third-party clients; Push notifications tell correspondence players that it is their move. Games are numerous and small, so the archive is write-heavy: Lichess's README states it keeps more than 12 billion games in MongoDB, indexed for search by Elasticsearch. A Relational database is a valid choice for a new build; for the archive consider an LSM-tree engine. Primary-backup replication and Point-in-time recovery protect users, ratings and games; deploys use Graceful draining and Process supervision. Metrics and hdr-histogram (no page yet) track move latency percentiles and pairing delay, and Mutual TLS authenticates internal calls. The rules and clock logic suit Deterministic simulation testing; PGN and FEN importers suit Coverage-guided fuzzing.

Data and control flow

A rated blitz game from click to result:

  1. The player clicks "3+2"; the app server adds them to the pool with rating, optional rating range and block list.
  2. A pairing wave scores all legal pairs and matches the pool; the game is created.
  3. Both sockets receive the game ID and join the game topic; the game actor starts with White's clock running.
  4. A move arrives with the user ID; the actor validates it, computes lag compensation from its own receive stamp, updates the clock and checks for game end.
  5. The actor persists the move, publishes it to the topic and resets the flag timer; spectators get the same encoded message.
  6. At game end (mate, resignation, flag, agreed draw) the actor writes the result, updates both ratings in one transaction, notifies tournaments, and enqueues analysis and fair-play checks.

Analysis of a finished game: the server enqueues a job; a worker evaluates every position and posts the result; the server stores it and derives accuracy. Failed jobs return to the queue after a timeout.

A fair-play case: a model scores an account from stored games and analysis; a score over the configured threshold puts a report in the moderators' queue; once a moderator marks the account, matchmaking pairs it with other marked players and refund jobs credit its opponents.

Scaling

In the order the limits appear:

  1. Concurrent connections. Memory and file descriptors per socket; scale the socket tier horizontally (it holds no game state) and spread topics over several pub/sub shards.
  2. Game state throughput. Each live game is an independent actor; shard by game ID across application servers. Broadcast-heavy events such as a big tournament are handled by sharing one encoded message among spectators.
  3. Matchmaking latency. Waves cap pairing cost; split pools by time control and rating band, and cap the batch, since weighted matching grows faster than linearly with pool size.
  4. Database write rate. Batch move writes, keep live games in memory, move the archive to an append-friendly store, replicate for reads.
  5. Analysis demand. Per-user and per-IP limits, priority queues (own games first), more workers; if workers are scarce, analyse on request only.

Failure modes

FailureSymptomCauseFix
Reconnect stormplayers dropped, then a load spikesocket tier restarted at oncedraining with staggered reconnect and jitter; keep games independent of connections
Clock unfairnesscomplaints of losing on time with a bad connectionno lag compensation, or lag credited without a capper-move cap, refilling quota, server-side stamps only
Lost move on crashposition differs after restartmove published before it was persistedpersist, then publish; rebuild the actor from storage
Rating inflation or deflationratings drift upward over timeunbalanced updates, provisional players farming, no deviationGlicko-2 with RD, exclude cheaters from pools, refunds
Rating manipulationaccounts feed wins to one accountpairing the same players repeatedlyforbid repeated pairings, detect boost patterns, pair flagged players apart
Engine cheatingunusually accurate play, uniform move timesexternal enginestatistical models plus human review, rating refunds, marks that affect matchmaking
Analysis backloganalyses delayed for hourstoo few workers, no request limitsper-user limits, priority queues, expire stale jobs
Sandbagging in tournamentsscores gained by losing on purposepoints rules with no safeguardsdraw and early-end rules: for example, Lichess awards no points for a draw within 10 moves

Real implementations

namewhat it isrepolicencenotes
lila (Lichess)the full server: lobby, game rounds, tournaments, puzzles, fair playlichess-org/lilaAGPL-3.0Scala; read the modules directory for the part split
lila-wsthe WebSocket tier, talking to lila over Redislichess-org/lila-wsAGPL-3.0separate process from the app server
lila-fishnetproxy between the server and analysis clientslichess-org/lila-fishnetAGPL-3.0queue for engine jobs
fishnetdistributed Stockfish analysis clientlichess-org/fishnetGPL-3.0runs on volunteers' machines with a personal key
scalachesschess rules, clocks and lag tracking, immutable and side-effect freelichess-org/scalachessMITused by lila
Lichess databasepublic exports of rated games, puzzles and evaluationslichess-org/databaseAGPL-3.0open data for research and training
Chess.comcommercial chess platform——public documentation only; internals not published

Sources

  • Lichess, lila README: components (Scala, separate WebSocket server over Redis, fishnet, MongoDB, Elasticsearch). github.com/lichess-org/lila
  • Lichess source files read: modules/pool/src/main/MatchMaking.scala and PoolActor.scala (waves, weighted matching), modules/irwin/src/main/IrwinApi.scala (thresholds and reports), modules/fishnet/src/main/FishnetLimiter.scala (request limits), and scalachess LagTracker.scala (lag compensation). lila modules, LagTracker
  • Lichess, lila-ws README: lila <-> redis <-> lila-ws <-> websocket <-> client. github.com/lichess-org/lila-ws
  • Lichess FAQ: Glicko-2, provisional ratings, leaderboard rules, time-control classes, cheating refunds. lichess.org/faq
  • Lichess, Arena tournament FAQ: scoring, berserk, pairing, draw rules. lichess.org/tournament/help
  • Lichess, Fair Play: what is and is not allowed. lichess.org/page/fair-play
  • Lichess open database of rated games, puzzles and evaluations. database.lichess.org
  • M. Glickman, Example of the Glicko-2 System. glicko.net

Similar systems

Share the most building blocks with this one.