Engineering Atlas › System Design Interviews

Design a multiplayer game backend

22 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional

Non-functional

Out of scope

Back-of-envelope

Assume 20M daily players, 1M peak concurrent players (CCU) and 10-player matches of 25 minutes (1,500 s). All figures are planning assumptions, not measurements.

API

Gameplay traffic is a custom binary protocol over UDP. Control-plane calls are ordinary gRPC or HTTPS.

# Control plane (HTTPS / gRPC), authenticated with the account token
POST /v1/tickets           {mode, party_ids[], skill{mu,sigma}, region_rtt_ms{"eu-w":28,"us-e":96}}
                           -> 201 {ticket_id, expires_in_s}
GET  /v1/tickets/{id}      -> {state: searching|found|cancelled|timeout,
                               match? {server_addr, port, join_token, expires_at}}
DELETE /v1/tickets/{id}
POST /v1/matches/{id}/result   (server-to-service only, mTLS)
                           {match_id, server_id, players[{id, stats, outcome}], seq}
                           -> 200 {applied: true}      # idempotent on match_id

# Data plane (UDP, one datagram = one message batch)
client -> server  INPUT     {conn_id, seq, ack_snapshot_tick, [ {input_seq, tick, move, aim, buttons} x3 ]}
server -> client  SNAPSHOT  {tick, last_input_seq_processed, baseline_tick, entities_delta[]}
both              RELIABLE  {msg_id, ack_bits, payload}    # chat, buy, round start
client -> server  CONNECT   {join_token, proto_version}     # signed by matchmaker, short TTL

Each INPUT repeats the last few inputs so one lost packet costs nothing. last_input_seq_processed tells the client which predicted inputs it can discard.

Data model

Two stores with different jobs, and one piece of state that deliberately lives only in memory.

High-level design

flowchart LR
  C["Game client"] -->|"HTTPS: login, queue"| GW["API gateway"]
  GW --> MM["Matchmaker (ticket pools)"]
  MM -->|"proposed match"| AL["Allocator (regional fleet manager)"]
  AL -->|"mark Allocated"| FL[("Warm game server fleet")]
  AL -->|"addr + join token"| MM
  C <-->|"UDP: inputs and snapshots"| GS["Game server process"]
  FL --- GS
  GS -->|"match result (mTLS)"| RS["Result service"]
  RS --> PS[("Profile + ledger store")]
  GS -->|"event stream"| AC["Anti-cheat analytics"]
  GS -->|"replay chunks"| OBJ[("Object storage")]

Queue to match path.

  1. The client posts a ticket with party, skill and measured round-trip time to each region (it pings a handful of regional endpoints at launch).
  2. The matchmaker keeps tickets in pools keyed by mode and region group. A match function scans a pool, picks players whose skill spread and latency fit the current rules, and balances teams.
  3. The allocator in the chosen region picks a Ready server from the warm fleet, marks it Allocated, and returns its address with a signed join token.
  4. The clients receive the address and send CONNECT over UDP. The server validates the token, and the match starts when all slots have connected or a timer expires.

In-match path. Each tick the server drains the input queues, steps the simulation, appends a snapshot to its history ring, builds one delta snapshot per player (filtered by visibility) and sends it. At the end it posts a signed result to the result service, which applies it once and then tells the server to shut down. The fleet manager replaces it with a fresh warm server.

Deep dives

1. Tick loop, state sync and lag compensation

Tick loop. The server queues inputs as they arrive and applies them on a fixed schedule, so the cost per match is predictable and does not depend on how fast players click (Gambetta). Simulation steps use a fixed delta, not wall-clock time, so a late tick does not change the physics. If a tick runs long, the loop runs catch-up steps up to a cap and then drops them, rather than spiralling.

Sync options. Three designs were considered. Lockstep sends only commands and needs every peer to be deterministic and to wait for the slowest, which suits RTS games and fails for 100 ms-sensitive shooters. Full state broadcast is simple but wastes bandwidth. Delta snapshots against an acknowledged baseline send only what changed since the last snapshot the client confirmed, and quantise values (positions to centimetres, angles to 10 bits); Gaffer On Games shows how far this compression can go. The choice is delta snapshots, with a full snapshot as the fallback when the baseline is too old.

Hiding latency. The client predicts its own movement immediately and reconciles when the server's answer arrives, replaying unacknowledged inputs on top of the authoritative state (Gambetta). Other players are drawn by interpolating between two past snapshots, so everyone else is seen slightly in the past.

Lag compensation. That past view makes shots miss. When the client fires it sends its timestamp and aim, and the server reconstructs the world as the shooter saw it, then judges the shot there (Gambetta). The server needs a ring buffer of recent positions, which is exactly the Practice below. The price is real: the victim can be hit shortly after reaching cover. Riot describes the other side of the same latency, peeker's advantage, and reduces it with 128-tick servers, minimal buffering and nearby servers (Riot). The rewind cap is a fairness dial: too high and high-ping players hit people who already ducked, too low and they cannot hit anything.

2. Matchmaking and session allocation

Matching. A ticket pool is scanned by a match function that proposes candidate matches, and a separate step accepts or rejects proposals so one player is not put in two matches. Open Match splits this the same way, with a frontend for tickets, a match function and a director, plus an evaluator to resolve conflicts (Open Match docs). FlexMatch expresses the rules declaratively, such as a maximum skill difference and a team structure, in a rule set (AWS docs). The design choice is the relaxation schedule: start with a tight skill band and a 40 ms latency cap, widen both every 10 seconds, and put a floor on quality so a 5-minute search ends in cancel and a retry, not a bad match.

Allocation. Starting a process takes seconds to tens of seconds (load the map, warm caches), so servers are pre-started and idle. With Agones on Kubernetes, a game server reports Ready through its SDK, and a GameServerAllocation flips one to Allocated so it will not be scaled down or reused (Agones GameServer). Its allocation spec supports label selectors with fallbacks and a Packed strategy that fills nodes so empty ones can be removed (Agones allocation). The alternatives are allocating on demand (cold starts, simple, wasteful) or letting the lobby host the match (cheap, but gives up authority and trust).

Regions. Pick the region from measured RTT, not from geography. For a mixed group, choose the region that minimises the worst player's RTT within a fairness bound, then check the bound: if one player would be at 180 ms, the matchmaker should keep searching or match that player into a closer pool. Keep warm capacity per region at a multiple of the expected arrival rate times the allocation time, plus a buffer for the daily peak.

3. Cheating and persistence

Authority first. The server validates every input: speed no higher than the movement rules allow, fire rate within the weapon's limit, and positions that stay inside the map. Out-of-range inputs are clamped or dropped and counted against the account, so a flood of them is a signal rather than a crash.

Information control. Authority does not stop a wallhack, because the client already holds enemy positions. The fix is to not send them. Riot's Fog of War marks enemies not relevant and stops replicating them unless line of sight is plausible, and it extends the check by velocity times a look-ahead longer than ping so players do not pop in late (Riot). The cost is CPU, so relevancy checks need their own share of the tick budget.

Persistence. A match is a transaction with the outside world, so the server never edits profiles. It posts one signed result keyed by match_id. The result service checks the signature, writes the match log entry and updates each profile conditionally on last_match_id, so a retry after a timeout is a no-op. Inventory purchases that happen mid-match go through the ledger service synchronously, not via the server. If the server dies before reporting, a watchdog marks the match void and nobody's rank moves, which is a better failure than guessing the winner.

Down the stack

Layer 1The contract

The player-facing promise is a bound on the delay between input and effect. With a fixed tick of 64 Hz, the server applies an input at the next tick boundary (up to 15.6 ms wait) and a snapshot reaches the client one half-RTT later. The contract for the network is weaker: datagrams may be lost, duplicated, reordered or delayed, and the application must tolerate all four (RFC 8085 sets out what a UDP application must do for congestion and checksums). For results the contract is stronger: a match result applies exactly once, even with retries.

Layer 2The mechanism

Each connection carries a sequence number and an ack field with a bitmask of the previous 32 received packets, so both sides learn about loss in one round trip without a handshake per message. The reliable channel is built on top: messages are resent until a packet that carried them is acknowledged. Snapshots are never resent; the next one is built from the latest acknowledged baseline, so loss only widens the delta. The server remembers for each client the last acked snapshot tick and builds its delta from that. Inputs are redundant (the last three in every packet), which is cheaper than retransmission timers. Snapshot history is a ring buffer of the last 20 to 64 ticks, enough for rewind and for baselines.

Layer 3The machine

A game server reads UDP from a socket with recvmmsg and writes with sendmmsg, batching many datagrams per syscall because each syscall costs roughly a microsecond or more and 30 snapshots per tick for 10 players is 300 sends per second per match before inputs. The safe payload is bounded by the path MTU: Ethernet gives 1,500 bytes minus 20 (IP) and 8 (UDP), so 1,472, and QUIC pins its minimum usable datagram at 1,200 bytes for the same reason (RFC 9000). Fragmented datagrams are lost whole if any fragment drops, so snapshots are split at the application layer. Receive buffers (SO_RCVBUF, capped by net.core.rmem_max) must hold a burst between ticks, and a socket whose buffer overflows silently drops. Per-core, the tick loop wants a pinned thread, no allocation on the hot path (a GC pause of 5 ms is a third of a 64 Hz tick), and cache-friendly entity arrays. Riot reduced its server frame from about 50 ms to under 2 ms by tuning code, hardware and OS settings, against a budget of 2.34 ms per frame derived from the 7.8125 ms 128 Hz tick, 3 games per core and 10% overhead (Riot). UDP has no connection state at the NIC, so one flooded port becomes a flooded server unless the edge filters it, which is why join tokens are checked on the first packet and cheaply.

Why it's built this way

  1. Light travels at about 200,000 km/s in fibre, so a 6,000 km path costs 30 ms one way. You cannot remove latency, so you hide it with prediction, interpolation and rewind.
  2. Latency has to be hidden by somebody, and a client that is trusted to judge hits can lie. So the server is the authority, and the client is a prediction engine that is always corrected.
  3. TCP delivers in order, so one lost packet stalls newer positions behind it. Old positions are worthless, so we use UDP and add reliability only where needed (High Performance Browser Networking, ch. 3).
  4. A server at tick rate T costs CPU in proportion to T, and a fleet of 8,000 cores is a business line. So the tick rate is tuned per mode, and every tick is measured.
  5. Matches are short-lived and cold starts are slow, so servers are pre-warmed, allocation is a flag flip, and the server is thrown away at the end rather than cleaned.

Worked example

Two players, Ana (RTT 40 ms) and Bo (RTT 120 ms), in a 64 Hz match with 100 ms of interpolation delay. Bo runs across open ground and Ana fires at the spot where Bo appears on her screen.

  1. Ana's screen shows Bo as he was 120 ms before she fires: 20 ms for the snapshot to reach her plus 100 ms of interpolation delay. She fires and sends her aim.
  2. Her packet reaches the server 20 ms later. The server computes the rewind as her full RTT plus interpolation, 40 + 100 = 140 ms, below the 200 ms cap, and looks up Bo's position at now - 140, interpolated between two ticks 15.6 ms apart.
  3. The aim point is 0.2 m from that position and the hit radius is 0.3 m, so it is a hit. Without compensation the server would test against Bo's current position, about 140 ms of running (roughly 1 m at 7 m/s) away, and Ana would have missed a shot that looked perfect.
  4. Bo, at 120 ms RTT, may have turned behind a crate 60 ms earlier on his screen. He sees himself die a moment after reaching cover. That is the cost, and it is bounded by the rewind cap.

Server-side the whole check is one array lookup, one interpolation and one distance compare per shot, well under 10 microseconds, which is why rewind is cheap compared with the snapshot build.

Failure modes

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Dedicated authoritative serversCheat resistance, fair hit rules, consistent latencyFleet cost, allocation latency, regional capacityCompetitive and ranked modes
Listen server (a player hosts)Almost free to runHost advantage, host can cheat, NAT problemsCasual co-op, small studios
Peer-to-peer lockstepTiny bandwidth, no serverWaits for slowest peer, needs determinismRTS with many units
128 tick vs 30 tickLower input delay, smaller peeker's advantageCPU and bandwidth scale with tick rateTactical shooters; not needed for slow games
Client-side hit detectionBest feel for the shooterTrivially cheatableNever for competitive play
Server rewind (lag compensation)Shots land where players aimedVictims are hit behind cover; needs history memoryHit-scan weapons
Warm pool vs on-demand serversInstant match startIdle costSteady, predictable demand
Strict vs relaxed matchmakingFair games vs short waitsQuality or wait time suffersTune per mode and population size

Leadership lens

Check yourself

  1. A player reports being killed after they had already reached cover. Is this a bug, and what two numbers control how often it happens?

    Show answer

    It is the designed cost of server rewind. It grows with the shooter's latency plus interpolation delay, and it is bounded by the maximum rewind cap, so lowering the cap trades this complaint for missed shots from high-ping players.

  2. Why not send enemy positions to every client and rely on the server to reject cheating inputs?

    Show answer

    Server validation stops fake actions but not reading data the client holds, so a wallhack works with perfectly valid inputs. The server has to withhold information that the player could not legitimately see.

  3. At 64 Hz, a match costs 0.8 ms of CPU per tick and you target 75% utilisation. How many matches fit on one core, and what happens at 128 Hz?

    Show answer

    0.8 ms x 64 = 51 ms per second, so 0.75 / 0.051 = about 14 matches per core. At 128 Hz, if cost per tick stays the same, it halves to 7, and the fleet doubles.

  4. A match server posts the result, the network drops the response, and the server retries. How do you prevent double-counting?

    Show answer

    Key the result by match_id and update each profile conditionally on last_match_id, so the second application is a no-op and returns the same success.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →