Design a Multiplayer Game Backend
Battle royale for 3 M concurrent players: 100-player matches on authoritative 30 Hz servers, fair hits, warm fleets and launch-day surges.
Last updated 2026-10-26. Difficulty: hard. Patterns: real-time, udp, game-servers, autoscaling, gaming. Reported at Epic Games, Riot Games, Activision, Electronic Arts, Roblox, Microsoft.
The interviewer asks, the candidate answers and draws, and you press Next. Skip to any stage or key moment, and ask the AI Mentor anything along the way.
Or sit it in the style of Microsoft, run as that company’s round is reported to go.
The short answer
How to design Multiplayer Game Backend in a system design interview, in five steps:
- Scope it. Log in and load the lobby, friends, presence and parties, start each match on a dedicated server within seconds, real-time movement, building and shooting for 100 players, matches of up to 25 minutes, rewards, stats and inventory after each match, store and wallet, cheat resistance.
- Set the targets. Scale: 3 M concurrent players · 30 k matches; Responsiveness: own input next frame · RTT < 80 ms; Fair hit detection: server decides · rewind ≤ 200 ms; Bandwidth per player: ~15 KB/s down · ~2.4 KB/s up.
- Do the numbers. Concurrent matches: ~30 k; Match starts per second: ~20/s · ~50/s at a season start; Downstream per player: ~15 KB/s (~120 kbps).
- Draw the design. Login queue, Waiting-room state, Platform gateway, Store & wallet, Payment platforms, Friends & parties, Matchmaking, Profile service, Player data store.
- Go deep where it is hard.
- Authoritative servers, not peers → Dedicated authoritative server per match; clients send only inputs.
- UDP, snapshots and the bandwidth budget → UDP: unreliable delta snapshots, a reliable event channel, interest management.
- Prediction, interpolation and lag compensation → Prediction, 100 ms interpolation, lag compensation capped at 200 ms.
- Allocating and scaling the fleet → Warm Ready pools on Agones, an allocator across clusters, forecast scaling and cloud bursting.
- Disposable servers, durable results → Count in memory, one result per player through a queue, compensate a crashed match.
- Launch day: the waiting room and the store rush → Waiting room with an admission rate driven by backend health.
Functional requirements
- Log in and load the lobby. Account, profile, locker and friends in a couple of seconds. When demand is more than the backend can take, a waiting room with a real position and estimate, never a wall of errors.
- Friends, presence and parties. Parties of up to four. The party leader queues; matchmaking is an existing service that takes the party and hands back a formed match of 100 players and a region.
- Start each match on a dedicated server within seconds. From match formed to every client connected in under 10 s: a server process in the chosen region, on the players’ build, already running.
- Real-time movement, building and shooting for 100 players. Your own movement responds on the next frame, other players move smoothly, and the server decides every hit, judged as the shooter saw it.
- Matches of up to 25 minutes. The map shrinks until one player or squad is left. Eliminated players leave early and can queue again at once, so a match empties as it goes.
- Rewards, stats and inventory after each match. XP, battle pass progress, quests, lifetime stats and item grants, applied exactly once per player per match and visible in the lobby within seconds of being eliminated.
- Store and wallet. Buy currency with real money through the platform stores and spend it on items. A purchase debits and grants atomically, exactly once, and a refund reverses it.
- Cheat resistance. The server rejects impossible inputs and never sends a player what they should not know. Client anti-cheat and statistical detection feed bans.
- Out of scope. Matchmaking internals (see Design a Matchmaking System), leaderboards (see Design a Real-Time Leaderboard), voice chat, rendering and game design, patch downloads to clients, and the internals of the anti-cheat driver.
Non-functional requirements
- Scale (3 M concurrent players · 30 k matches). Peaks follow the evening in each region. A season start or a big launch brings 2–3× the usual peak inside an hour, most of it in the first ten minutes.
- Responsiveness (own input next frame · RTT < 80 ms). Prediction hides the round trip for your own movement. Matchmaking keeps every player under ~80 ms to the region, and the 30 Hz tick adds up to 33 ms before an input is simulated.
- Fair hit detection (server decides · rewind ≤ 200 ms). This is the requirement that drives the hardest trade-off: players disagree about where everyone was, and one view has to win. Favouring the shooter, within a cap, is the choice.
- Bandwidth per player (~15 KB/s down · ~2.4 KB/s up). Has to fit poor home and mobile connections, and sets the egress bill: ~54 MB per player-hour and ~360 Gbps across the fleet at peak.
- Match start (allocation p99 < 2 s · all connected < 10 s). Players will not wait minutes for a machine to boot. Capacity must already be running in every region: allocation picks a warm server, it never creates one.
- Match reliability (< 1 in 1 000 matches lost). A crashed match is lost, not failed over. State changes 30 times a second, and replicating it costs more than the rare loss, so crashes are kept rare and compensated.
- Correct progression and money (rewards exactly once · purchases atomic). A result applies once per (match, player) however often it is delivered, and a purchase debits and grants in one transaction. A retry never mints currency.
- Live services (99.95 % · degrade, don’t fall over). Under a surge, logins wait at a fixed admission rate rather than letting retries take down the profile database. A match in progress never depends on the backend.
Back-of-envelope estimates
- Concurrent matches: ~30 k. 3 M concurrent ÷ 100 per match = 30 k. Two effects roughly cancel: about 10 % of players are in the lobby or the queue at any moment, while eliminated players leave early, so a running match averages fewer than 100 people. Plan on 30 k.
- Match starts per second: ~20/s · ~50/s at a season start. 30 k concurrent ÷ 1 500 s (25 minutes) = 20 matches/s, so 2 k players/s are placed on servers. Each of them later produces exactly one result, so the post-match pipeline writes ~2 k results/s. A season start reaches ~50 starts/s, paced by the login queue.
- Downstream per player: ~15 KB/s (~120 kbps). Interest management keeps a snapshot to the ~40 entities near you, and delta compression against the last snapshot you acknowledged leaves ~12 B per moving entity. ~40 × 12 B plus headers and events ≈ 500 B, × 30 Hz = 15 KB/s. Without either, the full state of 100 players is ~4 KB a tick (120 KB/s) and does not fit in one packet.
- Upstream per player: ~2.4 KB/s. An input packet is ~50 B of payload (sequence, acks, buttons, view angles, and the previous two inputs again in case of loss) plus 28 B of UDP/IP headers ≈ 80 B, × 30 a second = 2.4 KB/s. Clients send intent, never positions, so upstream stays tiny.
- Game server hosts at peak: ~1 500 (20 matches each). A match’s game loop is mostly single-threaded and needs about one core at its busiest (the landing, the final circles) plus ~2 GB of memory. A 32-core host runs ~20 matches and keeps the rest for networking and spikes: 30 k ÷ 20 = 1 500 hosts, plus the warm buffer.
- Egress per host: ~240 Mbps · ~360 Gbps fleet-wide. 100 players × 15 KB/s = 1.5 MB/s = 12 Mbps per match; × 20 matches = 240 Mbps per host, a few percent of a 10 Gbps NIC. Fleet-wide, 3 M × 120 kbps ≈ 360 Gbps. Packets bind before bits do: 20 × 100 × 30 = 60 k packets/s out of each host, and as many in.
- Warm buffer of ready servers: ~3.6 k (~12 % of the fleet). Bringing a new host into service takes ~3 minutes, so ready processes must cover the starts in the meantime: 20 starts/s × 180 s ≈ 3 600 servers across ~10 regions, ~12 % on top of 30 k. A season start is covered by raising capacity ahead of time, not by this buffer alone.
- Logins at a season start: ~5 k/s · ~50 k/s with retries. 3 M players opening the game within ~10 minutes = 5 k/s, against ~550/s on a normal evening (3 M sessions of ~90 minutes). Clients retrying every few seconds turn that into ~50 k/s of attempts. Each login reads a ~50 KB profile, so 5 k/s is ~250 MB/s from the player store. This, not the game servers, is what falls over first.
Components
- Login queue (waiting room): The front door during surges. Admits logins at a rate the backend has shown it can take and gives everyone else a signed ticket with a position and an estimated wait. The admission rate is a control loop on backend health, so neither demand nor retries can push load past it.
- Waiting-room state (Redis · two counters): Per region, the next ticket number (INCR on join) and the admitted watermark the admission controller advances by R each second. A position is ticket minus watermark, so the queue costs O(1) per player whatever its length. Tickets are signed and carry their number, so losing this store loses no one’s place.
- Platform gateway (sessions · WebSocket): Trades an admission token and a platform credential for a session, authenticates every lobby call and routes it to the live services. Holds one WebSocket per online player for party invites, presence and match.ready pushes. Nothing on a match’s real-time path goes through it.
- Store & wallet (transactional · ledger): Sells items for currency and currency for real money. Spending is one transaction on the player’s shard: insert the client’s purchase id (unique), append a debit to the wallet ledger if the balance covers it, insert the entitlement. Real-money credits are unique on the platform’s transaction id, and refunds post reversing entries.
- Payment platforms (console stores · PSP): PlayStation, Xbox, Nintendo, Apple, Google, Steam, and a card provider for direct sales. Each returns a signed receipt or a server-side purchase record we verify before crediting currency, and each sends its own refund and chargeback notifications that we must consume.
- Game client (predicts · interpolates): Samples input every frame and sends it to the game server 30 times a second over UDP. Predicts its own movement at once and reconciles against the server’s answer, and draws everyone else ~100 ms in the past, interpolating between snapshots. Runs the client anti-cheat, whose session token is required to join a match.
- Friends & parties (presence · party state): Friends lists, presence and parties. Presence is pushed only on state changes (online, in a match, back in the lobby), ~4 k/s, while details such as players left are read when someone opens the list. The party leader’s queue request goes from here to matchmaking with the whole roster, and the party survives a lost match because it never lived on the game server.
- Matchmaking (existing service): The existing matchmaking service (see Design a Matchmaking System), not designed here. Takes a party’s ticket, forms a 100-player match in a region chosen by measured latency, asks the allocator for a server, and pushes match.ready with the address and join tokens through the gateway.
- Profile service (account · progression · items): Owns the account (credentials check, bans), progression (XP, level, battle pass tier, quests), lifetime stats and the item inventory. Read once at login for the lobby, written mostly by the post-match pipeline, one transaction per result. Never called by a game server during a match.
- Player data store (sharded by account id): Accounts, profiles, inventory, the wallet ledger and the applied-results table, all keyed by account id on the same shard, so a reward or a purchase is a single-shard transaction. A sharded relational store (MySQL with Vitess, or Postgres) gives the unique constraints idempotency needs. A profile is ~50 KB, read once per login.
- Game server (authoritative · 30 Hz · UDP): One process per match, holding the whole match in memory. Every 33 ms it applies the inputs that arrived, simulates movement, building and damage, and sends each player a delta-compressed snapshot of only what is relevant to them. Validates every input, lag-compensates hits by rewinding hitboxes, and writes nothing to the backend until a player’s match ends.
- Regional fleets (Agones on Kubernetes): A few clusters per region, ~20 game server processes per host, each a pod with its own UDP port. Keeps a buffer of Ready servers that the forecast raises before peaks, bursts into cloud capacity when owned hosts run out, and never removes an Allocated server, which is how a build update drains: old servers finish their match and exit.
- Server allocator (match registry): Turns a formed match into a running server in about a second. Picks a cluster in the requested region with Ready servers on the right build, allocates one through that cluster’s fleet, and records the match (id, roster, server, start time, state) with a signed join token per player. Watches server health so a crash becomes a match.aborted event, not silence.
- Post-match pipeline (idempotent on (match, player)): Consumes results, checks each against the match’s roster and the playlist’s caps, and applies XP, quest progress, stats and item grants through the profile service in one transaction guarded by a unique (match id, account id) row. Turns match.aborted into time-played compensation. Leaderboards and anti-cheat analytics read the same topic as separate consumers.
- Results queue (Kafka · key = account id): The durable buffer between game servers and the backend. match.results holds one record per (match, player), a result when that player’s match ends or a match.aborted from the allocator, keyed by account id so a player’s records apply in order; match.events holds match.started with the roster, keyed by match id. Retains days, so a profile store outage delays rewards but never loses them.
User flows
- Match formed to first tick. Matchmaking hands over 100 players and a region; within seconds a warm server there is allocated, learns its roster, and every client is connected over UDP with a token. Nothing boots on this path.
- The party leader presses play; the friends service hands the whole party to matchmaking. The party is the unit: four friends become one matchmaking ticket and land on the same team. The client’s build version goes with it, because a match can only run on a server of the same build.
- Matchmaking forms a 100-player match in a region and asks the allocator for a server. Matchmaking’s job ends at a roster and a region chosen by measured latency. The call is idempotent on match id: a timeout and retry here would otherwise take two servers and split the roster.
- The allocator picks a cluster in that region with Ready servers on the build and allocates one. Allocation is a state change on a process that is already running with the map loaded, so it takes milliseconds to a second, not minutes. If the region’s clusters have no Ready server, the allocator tells matchmaking to hold the match for a few seconds rather than send it to a distant region.
- The allocator records the match in its registry and signs a join token for each player. The registry is what later tells a crash apart from a normal end, and which players to compensate. The game server verifies tokens itself with the allocator’s public key, so connecting needs no backend call.
- The game server learns it was allocated, reads its roster and opens a 60-second connect window. The process started minutes earlier, loaded the map and called Ready(). It sees the allocation through the SDK’s watch on its own record. Players who have not connected when the window closes are dropped from the roster and the match starts without them.
- Matchmaking pushes match.ready to every player through the gateway socket. All 100 should hear within a second: the bus leaves on a timer, and a player who connects late lands late. The push uses the socket the client already holds for party and presence, so there is no polling.
- Each client connects over UDP with its token; the server checks the token and the anti-cheat session. The challenge round trip means a packet from a spoofed address never gets a session, which matters because every player in the match learns the server’s address. From here on, client and server exchange 30 packets a second each way, encrypted with the session key.
- When everyone is in or the window closes, the match starts and the allocator publishes match.started. Formed to first tick in under 10 s: about a second to allocate, a second to push, a few seconds for clients to connect. Every one of those seconds is waiting on a network, never on a machine booting. match.started carries the roster, which the post-match pipeline uses to validate results later.
- One tick: move, shoot, hit. The real-time core. Inputs go up 30 times a second, the server simulates, judges a shot as the shooter saw it, and sends each player only what is near them, encoded against what they last acknowledged.
- The client samples input, applies it locally at once, and sends it with a sequence number. The client never sends a position, only what was pressed and where the player is looking. It runs the same movement code as the server, so its prediction is almost always what the server will compute.
- Server tick: apply every input received since the last tick, after validating it, and simulate. Movement axes are clamped to [-1, 1], turn rate to what a hand can do, firing to the weapon’s rate and ammo; a connection sending 3× the expected packet rate is throttled and flagged. All 100 players must be simulated inside the 33 ms budget, so the target is ~15 ms on average, leaving room for the landing and the final fights.
- A fire input arrives: the server rewinds the other players’ hitboxes to what the shooter saw and traces the shot. View time = now − the shooter’s latency − their interpolation delay, the method Valve documents for the Source engine. The server keeps ~1 s of hitbox history (30 ticks × 100 players, ~600 KB), rewinds only players near the line of fire, traces, and restores them. Rewind is capped at 200 ms, so a player on a 300 ms connection must lead targets instead of hitting players who are long gone.
- The server sends each client a snapshot of what is relevant to it, as a delta against its last acknowledged snapshot. Relevance comes from a spatial grid: what is in nearby cells, your squad wherever they are, anything shooting at you. Entities are ranked by distance and staleness and the packet is filled to a ~1 200 B budget, so distant players update at 5 to 10 Hz instead of 30. Events that must arrive (eliminations, pickups, the storm moving) ride a reliable channel inside the same packets until acknowledged.
- The client reconciles: it resets its own player to the server state at last_input and replays the inputs after it. If the server agreed, the replay lands where the player already is and nothing visible happens. A small disagreement is blended away over ~100 ms; a large one, such as being blocked by a wall someone built a moment ago, is a visible snap, rare by design.
- The client draws other players 100 ms in the past, interpolating between the snapshots around that time. Three snapshots of buffer at 30 Hz means one lost packet causes no stutter. The cost is that what you see of an opponent is ~150 ms old (their half of the trip, a tick, your half, the buffer), which is exactly why the server has to rewind when you shoot.
- The hit is confirmed in the next snapshot, and the victim’s client learns of it in the same tick. The shooter’s client may play a hit effect at once, but health changes only on the server’s word. The fairness cost lands on the victim, who may already be behind a wall on their own screen when the hit arrives; the rewind cap is what bounds it.
- Season launch: 3 M players at once. The scale-breaking case: demand arrives in minutes, not hours. Capacity is raised ahead from a forecast, logins wait in a queue at a rate the backend can take, and the first big purchase, the new battle pass, stays a single transaction.
- Hours before, the forecast raises each region’s fleet minimums and the new build is pre-pulled onto every host. Reactive scaling cannot meet a 2–3× peak that arrives in ten minutes when a host takes three to bring into service. The forecast starts from the same weekday’s curve per region, scales it by the factor seen at past launches, and requests burst capacity from the cloud provider days ahead, since everyone launching on the same morning can exhaust an instance type.
- Clients open the game; login is past its admission rate, so each gets a signed ticket and a position. A visible queue replaces 3 M clients each retrying every few seconds, which would put ~50 k requests a second on services sized for 5 k. Polls back off with position and are answered from a watermark each queue node caches, so millions of waiting players cost almost nothing.
- An admission controller advances the watermark each second, and a ticket below it is exchanged for an admission token. Additive increase, multiplicative decrease on admissions: the rate climbs while the backend is healthy and halves the moment it is not, so a struggling database slows the queue instead of failing logins already under way. It starts low (~2 k/s) while caches are cold after the season change.
- The admitted client logs in at the gateway, which checks the account and issues a session. The gateway checks the admission token’s signature locally. One login reads the account and a ~50 KB profile; at 5 k admissions a second that is ~250 MB/s from the player store, which is exactly the load the admission rate protects.
- The lobby loads: profile, locker, friends and presence, in one aggregated call. Presence is the first thing to shed under load: if the friends service is slow, the lobby renders without it rather than waiting. A lobby in two seconds matters more than a complete friends list.
- The player buys the new battle pass with currency: one transaction debits the wallet and grants the pass. BEGIN; insert the purchase id (unique); append a −950 ledger entry if the balance covers it; insert the entitlement; COMMIT, all on the player’s shard. A retry after a timeout hits the unique purchase id and returns the first result, so a flaky connection can never buy twice or pay without receiving.
- Another player tops up currency with real money; the store verifies the platform receipt and credits it once. The platform has already taken the money; our job is to credit exactly once and to reverse the credit when the platform later reports a refund or chargeback. A receipt that fails verification is kept for review rather than dropped, because a bug here costs real customers real money.
- Players queue, and the warm buffer absorbs the wave of allocations. The admission rate also paces the fleet: 5 k players admitted a second become at most ~50 match starts a second a few minutes later. If a region’s buffer still runs dry, matches wait in matchmaking for seconds with a visible message instead of going to a far region at 150 ms.
- Game server crashes mid-match. The failure path. The match dies with its process, by design. What matters is noticing within seconds, getting players back to the lobby with their party, and compensating exactly once.
- A game server process crashes 14 minutes in, with 41 players still alive. Usually it is one process, not the host: the other 19 matches on that host carry on, each in its own process on its own port. If the host itself dies, all 20 fail at once and the same path runs 20 times.
- Clients stop receiving snapshots and, after a few seconds of retrying, show “connection lost”. A client cannot tell a dead server from its own dead router, so it keeps sending to the same address for ~5 s first: a brief network blip must not end anyone’s match. Then it asks the gateway what happened.
- The allocator sees the server go Unhealthy, marks the match aborted, and publishes match.aborted for each player still in it. The 59 players eliminated earlier already have their results written, because a result is written when each player’s match ends, not when the server’s does. Only the 41 still alive get an aborted record.
- Clients ask the gateway, learn the match is gone, and return to the lobby with their party intact. The party lives in the friends service, not on the game server, so the squad is still together and can queue again at once. A clear message and a fast way back matter more to players than the lost match.
- The post-match pipeline turns each aborted record into compensation: XP for minutes played, no stats, no placement. Minutes played times the playlist’s average XP rate feels fair and is never better than playing well. Quest progress from the lost match is not credited: it was never written, and inventing it would be easy to abuse. Compensation is capped per account per day.
- The fleet replaces the capacity, and the crash is counted against the build’s crash budget. Matches lost to crashes are a measured budget, under 1 in 1 000. A new build whose crash rate is above the old one’s halts its own rollout automatically, which is far cheaper than any amount of replication.
- Match ends: rewards written once. The asynchronous path. Each player’s result leaves the game server once, when their match ends, travels through a durable queue, and is applied exactly once to progression, stats and inventory, even when everything in between retries.
- A player is eliminated in 37th place; the server finalises their result from counters it kept in memory. Time alive, eliminations, damage, chests opened and quest counters have been counted in memory all match. Nothing was written during play, so the backend saw no writes for this player for 14 minutes.
- The server publishes a signed result keyed by account id and keeps it until the broker acknowledges. Keyed by account id so all of one player’s results apply in order on one partition. The result is signed with the server’s fleet-issued identity, so a result from anywhere else is rejected.
- The pipeline checks the result: the player was on this match’s roster, and the numbers are within the playlist’s caps. The roster comes from match.started on the same queue. This is the backend’s own anti-cheat boundary: even a compromised game server cannot grant rewards to accounts that were not in its match, or more XP than a match can earn. Rejected results go to a review queue, not the floor.
- The profile service applies the result in one transaction, guarded by a unique (match id, account id) row. The unique row turns at-least-once delivery into exactly-once effects: a redelivered or replayed result inserts nothing and changes nothing. Level and battle pass tier are derived from total XP inside the transaction, so results applied in any order land on the same profile.
- The pipeline commits its offset only after the transaction commits. In that order, a crash between the two causes a duplicate, never a loss, and the unique row makes the duplicate harmless. Committing the offset first would make the same crash lose a reward silently.
- The player, already back in the lobby, sees their XP and battle pass progress within seconds. If the profile store is degraded, results wait in the queue, retained for days, and players see “rewards pending” for minutes instead of losing anything. Game servers are unaffected: they let go of the result at the broker’s acknowledgement.
- When the match ends, the server shuts down; the fleet replaces it with a fresh Ready process. A server never takes a second match: a fresh process means nothing leaks from one match to the next. During a build rollout this is also the drain: finished servers are replaced on the new build, so the fleet changes version within one match length without ending a single match.
Deep dives
Authoritative servers, not peers
Why pay for 1 500 servers? Why not let a player’s machine host the match, or let each client report its own position?
Dedicated servers are the biggest line in the bill: ~1 500 hosts at peak and ~360 Gbps of egress. Peer-to-peer and client authority both look cheaper. But in a 100-player match a peer host would receive 99 players’ inputs and send 99 snapshot streams from a home connection, its own latency would be zero (an unfair edge), the match would die when the host quits, and NAT traversal fails for a few percent of players anyway.
The deciding observation is the trust boundary: anything a client computes, a cheater controls. If clients report positions, a speed hack is one edited number; if they report hits, an aimbot does not even need to aim. If clients only send inputs (which buttons, which way they look) and one server simulates the world, the worst a cheater can do is press buttons very well, and the server can also withhold what a player should not see.
- Dedicated authoritative server per match; clients send only inputs chosen
- Client authority: clients send positions and hits, the server relays rejected
- Peer-to-peer with one player as host situational: small private or cooperative sessions, or 1v1 fighting games that exchange inputs with rollback, where a server would only add a hop
- Deterministic lockstep: every client simulates, only inputs are exchanged situational: real-time strategy with thousands of units, where inputs are far smaller than the state
The answer: Dedicated authoritative servers. Clients send inputs 30 times a second; the server simulates at 30 Hz and is the only source of truth for positions, health, inventory, building and hits. It validates every input (movement axes clamped, turn rate within human limits, fire rate and ammo per weapon, packets per second per connection) and sends each player only what is relevant to them. Prediction hides the round trip for your own movement and lag compensation makes aiming fair, so authority costs the player nothing visible. Client anti-cheat is the second layer, at the boundary: the connect handshake requires a valid anti-cheat session token, and its detections, together with statistical checks on match telemetry, feed bans. Tick rate is a cost dial: 30 Hz suits 100 players on a large map, while 5v5 tactical shooters spend more per match on 64 or 128 Hz (Riot runs VALORANT at 128) because they have 10 players, not 100.
Why 30 Hz and not 60?
Cost against what the game needs. A 100-player match at 60 Hz roughly doubles CPU per match, so ~10 matches per host instead of 20 and ~3 000 hosts, and doubles snapshot bandwidth to ~30 KB/s per player. The gain is ~17 ms less average wait before an input is simulated. For a battle royale with long sightlines, 30 Hz with good interpolation is the sensible point; a 10-player tactical shooter, where duels are decided in milliseconds, is where 64 or 128 Hz pays for itself.
Couldn’t you trust the client for movement and only check it for plausibility?
Some games do: accept the client’s position if it is within maximum speed of the last one. The trouble is that every check becomes a tolerance, and cheaters tune themselves to sit just inside it, moving 10 % faster everywhere forever. Inputs-only is simpler to secure because there is no tolerance to exploit: the server computes the position itself. The cost, a round trip, is already paid for by prediction.
What does the server still have to trust the client for?
Where the player is looking, when they pressed the button, and what they were seeing (the view time used for lag compensation). Aim is the hole: an aimbot sends perfectly valid inputs with superhuman precision. So the server caps turn rate and bounds the claimed view time, and the rest is detection: client anti-cheat looking for injected code, and statistics on match telemetry such as reaction times and snap angles.
How does an authoritative server stop wallhacks?
By not sending what the player cannot see. Interest management already limits snapshots to nearby entities; going further, the server checks line of sight and includes enemies only when they are potentially visible, slightly early so they do not pop in. Riot has written publicly about doing this in VALORANT. It costs server CPU per player per tick, so a 100-player game usually does it more coarsely than a 10-player one.
Can one match use more than one core?
Partly. The loop that steps the simulation is hard to parallelise because everything interacts with everything, but serialising 100 snapshots, physics queries and network I/O can move to worker threads, and engines do some of this. The bigger lever is many matches per host: 20 single-threaded matches use 20 cores in parallel with no locks between them, which is why per-match CPU is budgeted in cores.
UDP, snapshots and the bandwidth budget
Why UDP instead of TCP or WebSockets, and how do you fit 100 players into 15 KB/s?
TCP delivers in order, so one lost packet holds back every packet behind it until the retransmission arrives: at 1 to 2 % loss and a 60 ms round trip, the game freezes for at least a round trip, and 200 ms or more when the retransmission timer fires, every few seconds. And what it holds back is stale: a position from 33 ms ago is worthless once the next one arrives. Naively, the state of 100 players at ~40 B each is 4 KB a tick, 120 KB/s to every player, and it does not fit in one packet.
Most game state is latest-value-wins, so it should be sent unreliably and superseded rather than retransmitted. The few things that must arrive (an elimination, an item pickup, the storm moving) are small and can ride inside the same packets until acknowledged. And each client needs only what is near it, and only what changed since the state it already has.
- UDP: unreliable delta snapshots, a reliable event channel, interest management chosen
- TCP or WebSockets for everything rejected
- QUIC datagrams or WebTransport situational: browser games, or a team that wants standard encryption and congestion control under its own snapshot logic
- Full snapshots every tick, no deltas rejected
The answer: A custom protocol over UDP. Each packet carries its sequence number, the latest sequence received from the other side and a 32-bit field acknowledging the 32 before it, so both ends know exactly which packets arrived without any retransmission queue. Snapshots are unreliable: the server keeps the last 32 it sent each client (~1 s), encodes each new one as a delta against the newest that client acknowledged, and falls back to a full snapshot if nothing was acknowledged for a second. Events that must arrive go in a reliable channel, repeated in every packet until acknowledged. Interest management uses a ~100 m grid plus a priority accumulator to fill a ~1 200 B budget with the most relevant entities. Positions are quantised (1 cm on a 2.5 km map is 18 bits per axis), angles to 16 bits, and unchanged fields cost one bit. The result is ~500 B per snapshot: 15 KB/s down and 2.4 KB/s up per player.
A player is on 3 % packet loss. What do they experience?
Very little for movement: a lost snapshot is replaced by the next one 33 ms later and the interpolation buffer covers the gap, and their inputs ride three times so the server rarely misses one. Deltas get a little bigger because the baseline is older, and reliable events arrive a packet later. Past ~10 %, or with bursts of loss, the buffer runs dry, other players stutter, and the client shows a connection warning.
At the start, all 100 players are on the bus within a few metres of each other. Does interest management still help?
Not much, since everyone is relevant to everyone. The priority accumulator still bounds each packet at ~1 200 B: the nearest and most-changed entities go every tick and the rest every second or third tick. On the bus there is little to send anyway, because everyone moves together; the heavy moment is the landing, which is why per-match CPU and bandwidth are budgeted for the busiest minutes, not the average.
Why not just run snapshots through zlib?
A general-purpose compressor on a 500 B packet saves little and costs CPU on 3 000 packets a second per match. The large wins are domain-specific: only what changed since the acknowledged baseline, quantised to the precision the game needs, only for relevant entities. A small static entropy coder trained on real traffic adds perhaps 20 to 30 % on top, which is worth having at 360 Gbps.
Some networks block UDP. What then?
A fallback that tunnels the same packets over TCP or a WebSocket on port 443 through a relay, accepting the head-of-line stalls. It serves the few percent of players on restrictive networks, and the client tells them their connection is degraded. The game protocol does not change, only the transport underneath it.
Every player learns the server’s IP address. How do you stop one of them knocking it offline?
Packets that do not authenticate with the session key are dropped before any game logic, so junk floods are cheap to discard, but a volumetric flood still fills the link. So game traffic enters through providers with DDoS scrubbing at the edge, ports are per match and short-lived, and some studios route players through their own relay network so the real server address is never exposed. Rate limits per source address protect the connect handshake.
Prediction, interpolation and lag compensation
Your ping is 60 ms and the server is authoritative. How does movement feel instant, and whose screen decides whether a shot hit?
With pure authority, pressing forward moves you a round trip plus up to a tick later, ~60 + 33 ms, which feels like wading through mud. Other players arrive in discrete 30 Hz snapshots with jitter, and drawing them as they arrive looks like stutter. Worst of all, what you aim at is ~150 ms old (their half of the trip, a tick, your half, the render buffer): a sprinting player covers ~0.9 m in that time, a full body width, so without correction you would have to lead every target by your latency.
Each technique moves the latency somewhere it hurts less. Predict yourself, because only you know your inputs. Draw others slightly in the past, because smoothness matters more than 100 ms of age. Judge shots in the shooter’s past, because that is what they aimed at. The last one moves the cost onto the target, which is why it needs a cap.
- Prediction, 100 ms interpolation, lag compensation capped at 200 ms chosen
- No lag compensation: shooters lead their targets rejected
- Client-side hit detection, checked by the server for plausibility situational: cooperative or casual games where a cheated hit costs little
- Extrapolate other players forward instead of interpolating situational: vehicles and other slow, predictable objects
The answer: The client runs the same movement code as the server on its own inputs and shows the result at once, keeping a buffer of inputs the server has not acknowledged; each snapshot carries the last input processed, and the client resets to that state and replays the rest, blending small errors and snapping only above ~1 m. Other players are drawn 100 ms in the past, interpolating between the snapshots around that time, which survives a lost packet. Each fire input carries the tick the client was rendering; the server rewinds other players’ hitboxes to that time (now minus the shooter’s latency and interpolation delay, the method Valve documents for the Source engine), traces the shot, and restores them. Rewind is capped at 200 ms: players under it hit where they aimed, players over it must lead. The trade-off is stated plainly: favour the shooter within the cap, and accept that a victim occasionally dies just after reaching cover.
The victim says they were already behind a wall when they died. Who is right?
Both, on their own screens. The shooter saw them in the open 100 to 150 ms earlier and the server judged the shot in that past, which is the deliberate choice. The cap limits how far back this can reach, and matchmaking keeping everyone under ~80 ms keeps it to a fraction of a second. A killcam showing the shooter’s view helps players accept it.
How does reconciliation avoid jitter when the server disagrees slightly?
It does not snap for small errors. If the corrected position is within a few centimetres, the simulation uses it at once but the rendered position blends toward it over ~100 ms. Only above a threshold, say 1 m, where something real happened such as a wall built in your path, does it snap. Identical movement code and fixed timesteps on both sides keep disagreements rare in the first place.
What does lag compensation cost the server?
Memory: ~1 s of hitbox poses for 100 players at 30 ticks, ~600 KB per match. CPU: per shot, rewind only the players near the line of fire, trace, restore. An automatic weapon fires ~10 times a second, so a busy fight is a few hundred rewinds a second, small next to the simulation itself. Slow projectiles are simulated forward on the server instead of being rewound.
Could a cheater lie about their view time to rewind further?
They can claim anything, so the server bounds it: the claimed view time must fall within the measured round trip plus the interpolation delay and a small tolerance, and never past the 200 ms cap. A claim outside that is clamped and counted, and a client that keeps doing it is flagged. It is the general rule: anything a client reports is a hint, bounded by what the server measured itself.
How would you know the netcode is working?
Per region and per build: server tick time p99 against the 33 ms budget, snapshot size, loss and round trip per player, prediction corrections per minute and their size, and hit-registration complaints per thousand matches. A jump in corrections after a patch usually means client and server movement code have drifted apart.
Allocating and scaling the fleet
Matchmaking hands you 100 players. How do they get a server in the right region within two seconds, at 20 matches a second and 50 on launch day?
Booting a VM and pulling a multi-GB server image takes minutes; even starting a process on a running host and loading the map takes ~30 s. Players will not wait for either, so capacity must already be running and allocation must be a choice, not a creation. The fleet is ~1 500 hosts at peak across ~10 regions whose peaks follow their own evenings, and a match lasts 25 minutes and cannot be moved, so a host with a live match on it cannot be removed or patched.
The shape of the answer is layers of capacity by lead time: ready processes answer in a second, spare hosts refill them in ~30 s, new hosts arrive in ~3 minutes, and a forecast covers hours. And "allocated" must be a state that protects a server from everything that would otherwise kill it: scale-down, rollouts, node maintenance.
- Warm Ready pools on Agones, an allocator across clusters, forecast scaling and cloud bursting chosen
- Start a server on demand for each match rejected
- One VM per match situational: matches heavy enough to need a whole machine, or a need for strong isolation between matches
- Managed game server hosting situational: a small studio without a platform team, or a game whose peak is a few hundred hosts
The answer: Each region runs a few Kubernetes clusters with Agones, the open-source game server orchestrator. A Fleet per (build, playlist) keeps a buffer of Ready servers: processes already started, map loaded, waiting. The allocator picks a cluster in the requested region by Ready count and health and sends a GameServerAllocation with label selectors; Agones flips one server to Allocated, and Allocated servers are never scaled down or replaced. Packed scheduling fills hosts before starting new ones so the cluster autoscaler can remove empty hosts at the end of a peak. Capacity comes in layers by lead time: Ready processes, ~12 % of the fleet, answer an allocation in a second and are sized to last the ~3 minutes it takes to add a host; spare host headroom lets the fleet refill that buffer in ~30 s a process; and a per-region forecast sets minimums an hour ahead and books burst capacity before a season start. Baseline load runs on reserved or bare-metal hosts, and peaks burst into on-demand cloud hosts in the same metro. Updates drain: the new build’s Fleet scales up, the old one’s Ready count goes to zero, and its Allocated servers finish their match and exit, so a version change takes about one match length.
Packed scheduling puts 20 matches on one host. Isn’t that a big blast radius?
Yes, and it is a deliberate trade. A host failure ends 20 matches, 2 000 players, at once. Spreading would lose only one or two per host, but it leaves every host partly full, so the autoscaler can never remove any and the fleet costs more after every peak. Hosts fail far less often than processes crash, so pack, and spread each cluster across availability zones so a zone failure is a fraction of a region.
A region’s Ready buffer hits zero during a launch. What happens?
The allocator tries the region’s other clusters first. If all are empty it answers “no capacity, retry in a few seconds”, and matchmaking holds the match with players told it is finding a server. It does not spill into another region by default, because a 150 ms match is worse than a 10 s wait; a neighbouring region within everyone’s latency limit is a fallback after a longer wait. Meanwhile the fleet starts processes on spare hosts (~30 s) and the cluster adds hosts (~3 min).
How do you ship a hotfix when matches last 25 minutes?
Blue-green per Fleet: create the new build’s Fleet with a Ready buffer, switch the allocator’s selector to the new build, and scale the old Fleet’s Ready count to zero. Old Allocated servers finish naturally, and within ~25 minutes the old Fleet is empty. Clients must be on a compatible build, so the client patch and the selector switch go together, with matchmaking requiring the right build on each ticket. Ending old matches early is possible for a severe bug, as a product decision with compensation.
How do you forecast capacity for a season start?
Take the same weekday’s concurrency curve per region, scale it by the ratio seen at the last few launches, adjusted for pre-registrations, and add a margin. Raise fleet minimums an hour ahead, pre-pull the new image to every host, and book burst capacity from the cloud provider days ahead, since a region can run out of one instance type when several big launches land on the same morning. Reactive buffers then absorb the forecast’s error.
What do you measure to know the fleet is healthy?
Allocation latency p99 and failures per region; the Ready buffer expressed as minutes of demand (Ready count ÷ start rate); host utilisation and the share of hosts with no Allocated servers, which should be draining away; tick time p99 per host, where a noisy neighbour shows up first; and crashes per 1 000 matches per build.
Disposable servers, durable results
Why not save progress to the database as the match goes, and why not replicate the match so a crash doesn’t lose it?
A match produces something worth rewarding every few seconds per player: eliminations, chests, distance, quest steps. Writing each as it happens is 3 M ÷ 5 s ≈ 600 k writes a second for values that matter only when the player’s match ends, and it makes the tick wait on the backend, so a slow database becomes a stutter for 100 people. Replicating the match instead means shipping the whole world (100 players, every built wall, every projectile) 30 times a second to a standby, or running a bit-exact twin, roughly doubling the fleet; and failover would still freeze play for seconds while clients move to a new address.
The game server is the right place to count and the wrong place to keep anything. Everything that must survive (the party, earned progression, purchases) lives outside it and is written once per player when their match ends, through a durable queue that turns every retry into a harmless duplicate. Then a crash loses only the match itself, which at under 1 in 1 000 matches is cheaper to compensate than to prevent.
- Count in memory, one result per player through a queue, compensate a crashed match chosen
- Write progress to the profile database during the match rejected
- Hot standby replica of every match rejected
- Periodic checkpoints of world state, restored on a new server situational: persistent worlds (MMO zones, survival servers) where hours of player-built state live on the server
The answer: The game server counts everything in memory and, when a player’s match ends (eliminated, quit or won), publishes one signed result to Kafka keyed by account id with acks from all replicas. The post-match pipeline checks it against the roster from match.started and the playlist’s caps, then applies it through the profile service in one transaction on the player’s shard: insert (match_id, account_id) into applied_results, which is unique, then update XP, level, battle pass, quests and stats and insert item grants; a duplicate inserts nothing and stops. It commits its Kafka offset only after the transaction, so a crash causes a duplicate, never a loss. A crashed server is not failed over: Agones marks it Unhealthy within ~6 s, the allocator emits match.aborted for the players still alive, and the pipeline grants XP for minutes played, without stats or placement, through the same unique row. Crash rate per build is a release gate, which is what keeps losses under 1 in 1 000.
The profile store is down for 20 minutes at peak. What happens?
Matches are unaffected: servers hand results to Kafka and carry on. About 2 k results a second accumulate, 2.4 M in 20 minutes, a few GB. Players see “rewards pending”. When the store recovers, the pipeline drains the backlog at a rate capped so it does not starve logins, which takes minutes; nothing is lost and nothing applies twice.
Why key the queue by account id rather than match id?
Because the write lands on the account’s shard, and ordering matters per account, not per match: two results for the same player after a quick re-queue should apply in order so a level-up reward is granted once, at the right tier. Keying by match would spread one partition’s writes across every shard and let one player’s results race each other.
A whole host dies. What is different?
Twenty matches abort at once instead of one: ~2 000 players ask the gateway about their match within seconds, and the pipeline gets ~1 000 aborted records. Both are small loads. The thing to watch is correlated failure: if hosts die together (a zone, a bad kernel update), that region’s Ready buffer drops too, so clusters span zones and host maintenance is staggered.
A bug in the new build crashes 5 % of matches at minute 20. What happens?
The crash-rate-per-build alert fires within minutes, while the rollout is still a small share of the fleet. The rollout halts and the allocator’s selector goes back to the old build for new matches; servers on the bad build keep running until they finish or crash. Compensation runs for every aborted match automatically. This only works if clients can play on either server build, which is why client and server compatibility is versioned.
Could players farm the compensation?
Only by crashing servers on purpose, which would be an exploit to fix urgently. Compensation is minutes played times the playlist’s average XP rate, so it is never better than playing, and it is capped per account per day. Accounts that turn up in crashed matches far more often than chance are flagged for review.
Launch day: the waiting room and the store rush
A new season starts at 9 a.m. and 3 million players open the game within ten minutes. What keeps login, the lobby and the store up?
A normal evening sees ~550 logins a second; the season start asks for 5 k a second, and clients that get an error retry every few seconds, so attempts reach ~50 k a second. Each login fans out to the account, profile, inventory, friends and store, and reads a ~50 KB profile from a database whose caches went cold with the new season. Databases do not scale out in ten minutes, and a backend pushed to 120 % does not serve 100 %: timeouts cause retries and throughput collapses. Epic’s public post-mortem of the February 2018 Fortnite outage, at 3.4 million concurrent players, is mostly about these shared services, the profile database in particular, rather than the game servers.
The insight is to make admission explicit: decide how many logins a second the backend can take, let exactly that many in, and tell everyone else where they stand. A visible queue turns a retry storm into a predictable, fair wait. And the first thing many players do once in is buy the new battle pass, so the store must stay exactly correct under that rush, because a purchase is money.
- Waiting room with an admission rate driven by backend health chosen
- Autoscale the backend to meet the spike rejected
- Return 503 and rely on client back-off rejected
- Stagger the release by region or account cohort situational: the launch time is yours to choose and regions would peak at different hours anyway
The answer: A waiting room in front of login, per region. Joining is an INCR on a tail counter in Redis, which yields a ticket signed with its number; an admission controller advances an admitted watermark by R every second, and a poll whose number is below the watermark gets a short-lived signed admission token that the gateway checks without calling anyone. R is a control loop on backend health: it grows ~5 % a minute while the profile store’s p99 and error rate are healthy and halves when they are not, starting low (~2 k/s) while caches warm. Position is ticket minus watermark and the estimate is position ÷ R; polls back off with position and are answered from a watermark cached on each queue node, so 3 M waiting players cost almost nothing. Ahead of the start, fleets and stateless tiers are pre-scaled from the forecast, and the lobby sheds optional parts (presence, news) before essential ones. The store is never queued or eventually consistent: a purchase is one transaction on the player’s shard keyed by a client purchase id, debiting the wallet ledger only if the balance covers it and granting the item in the same commit.
A queue node crashes. Does everyone lose their place?
No. The ticket is signed and carries its number, so any node can answer a poll; the only shared state is two counters per region. If Redis itself is lost, the controller restores the watermark from its last logged value and sets the tail above the highest ticket number seen. A few players may be admitted slightly out of order, which is acceptable for a waiting room.
A player double-taps buy on a slow connection. What happens?
The client generates one purchase id when the player confirms and reuses it on every retry. The second request hits the unique purchase id, finds the first transaction committed, and returns its result: one debit, one item. If the first never reached the database, the second performs the purchase. A new id is minted only when the player starts a new purchase, the same contract as a payment idempotency key.
A platform refund arrives for currency the player has already spent.
Post a reversing ledger entry. The balance may go negative, and a negative balance blocks further purchases until topped up. Whether to revoke items already bought with that currency is a policy decision within each platform’s rules; repeat abusers are banned. The ledger keeps the full history auditable, the same reasoning as a payment ledger.
How do you choose the starting admission rate?
From load tests and the last launch: the rate at which the profile store’s p99 stayed under target with cold caches. Start below it and let the control loop climb. The most valuable rehearsal before launch day is a game day that replays a recorded login surge against a production-sized staging environment to find the real limit.
The daily item shop changes at midnight UTC and everyone opens it at once.
The catalogue is the same for everyone, so render it once and serve it from a CDN keyed by the rotation time, with clients fetching it with jitter over a minute. Only purchases reach the store service, and those are spread out by human decision time. Personalised offers are precomputed before midnight so the per-player part is off the critical minute.
The theory behind it
- Multiplayer game servers: How real-time multiplayer games keep players in sync: authoritative servers, tick rates and snapshots, client prediction and reconciliation, lag compensation, UDP networking, interest management, server allocation and fleets, persistence, cheating and scaling to large worlds.
- Autoscaling and capacity planning: Horizontal versus vertical scaling, what to autoscale on, why autoscaling is too slow for spikes, warm pools and pre-scaling, serverless trade-offs, headroom, and how to plan capacity from numbers.
- Stateful vs stateless services: Why stateless services scale and recover easily, where state has to live instead (databases, caches, sessions, object storage), and how to run genuinely stateful services such as WebSocket gateways, game servers, caches and stream processors with sticky routing, partitioning, replication and draining.