← System design

Gaming

Design Design PUBG

A battle royale looks like a game problem, but underneath it is a hard real-time distributed system: one server per match must simulate 100 players every 33 ms while each of them sits behind a different, unreliable connection. Around that sits a conventional platform for parties, matchmaking and stats, and a fleet that has to start a fresh match server about fourteen times a second.
Core: authoritative tick loop with prediction and rewind26,000 concurrent matches at peakLatency-critical UDPEphemeral per-match servers

01

The problem

01

The problem

A battle royale match puts 100 people on one island for about half an hour. They parachute in, loot buildings, drive vehicles, shoot each other from a few metres or from half a kilometre, and a closing circle herds the survivors together until one squad is left. Every one of those players needs to believe that what they see on screen is what really happened.

Two very different systems sit behind that experience. The platform is ordinary backend work: accounts, parties, matchmaking, store, stats, patches. The match itself runs on a dedicated server process that owns the simulation, advances it in fixed steps called ticks, and streams the result to every client 30 times a second. That process lives for one match and then disappears.

The hard part is the second half. Players are spread across home connections with 20 to 150 ms of round trip and occasional packet loss, and the game still has to feel instant when you press a key and fair when someone dies.

The question to answer

How does one server keep 100 players, thousands of loot items, vehicles, grenades and a moving zone consistent for everyone, at under one frame of perceived input delay, while the network drops and reorders packets, and while some players are actively trying to cheat?

02

What the game needs

02

What the game needs

Functional
  • Players queue solo, as a duo or as a squad of four, pick a region and a mode, and get placed into a 100-player match within about a minute.
  • Inside the match: movement, vehicles, looting, inventory, hitscan and projectile weapons, healing, knock-downs and revives.
  • A play zone shrinks in phases and damages anyone outside it.
  • Squad voice and text chat, plus spectating a teammate after you die.
  • After the match, placement, kills and damage feed into stats, ranked rating and rewards within a minute or two.
  • Server-side replays of every match, so reported players can be reviewed and highlights can be rendered.
Non-Functional
  • Local input feels immediate: your own movement and firing show up on the next rendered frame, never after a network round trip.
  • Server simulation at a steady 30 Hz, so the whole tick must finish inside 33 ms even with 100 live players.
  • Playable on round trips up to about 150 ms and with 2 to 3% packet loss.
  • The server is the only authority on position, health, ammo and hits. A modified client cannot grant itself anything.
  • Fewer than 1 in 2,000 matches lost to a server crash.
  • Platform services degrade separately: a broken stats service must not stop matches from starting.
Ask before you draw

Pin down tick rate, region list, and whether ranked play exists. Tick rate sets your CPU budget and bandwidth bill. The region list decides how many fleets you run. Ranked play means match results become valuable, so cheating and result tampering move up the priority list. It is also worth asking whether console and PC share matches, since that changes input handling and anti-cheat options.

03

Back-of-envelope numbers

03

Back-of-envelope numbers

Assume 3 million players online at the global peak, with about 87% of them in a match and the rest in menus. Matches last 30 minutes on average. The server runs at 30 Hz and the client sends input at the same rate.

QuantityWorkingResult
Players in matches at peak3M x 0.87about 2.6M
Concurrent matches2.6M / 100about 26,000
Match starts26,000 matches / 1,800 s eachabout 14 per second
Players placed by matchmaking14 x 100about 1,400 per second
Snapshot size per client per tickdelta against last acked state, quantizedabout 600 B
Downstream per player600 B x 30 ticks18 KB/s, about 144 kbps
Upstream per playerabout 70 B of input x 30, with the last 3 inputs repeatedabout 2 KB/s
Egress per match100 x 18 KB/s1.8 MB/s, about 14 Mbps
Fleet egress at peak26,000 x 14 Mbpsabout 375 Gbps
Game hosts8 matches per 16-core host, plus 15% warm spareabout 3,750 hosts
Matches per dayaverage load about half of peak, 13,000 x 48 slotsabout 620,000
Replay storage620,000 x 30 MB compressed, kept 14 daysabout 18.6 TB a day, 260 TB held
What the numbers say

The platform tier handles roughly 1,400 matchmaking placements a second, which is a normal web workload. The game tier is the expensive one: thousands of hosts, hundreds of gigabits of UDP, and a hard real-time deadline on every tick. Most of the design effort and most of the money go there.

CPU is front-loaded

A match is heaviest in its first five minutes, when all 100 players are alive and clustered near drop points. By minute twenty perhaps 25 remain. Packing hosts by average load will overrun ticks at match start, so pack by peak load per match, or stagger starts on each host so they do not all peak together.

04

APIs and the wire protocol

04

APIs and the wire protocol

There are two very different interfaces. Menus, parties and matchmaking talk to the platform over HTTPS and a WebSocket for push. Once a match is assigned, the client opens a custom UDP channel straight to the game server, and the platform is out of the loop until the match ends.

Platform calls

POST/v2/queue/ticketsPut a party into matchmaking
// Authorization: Bearer <session jwt>, sent by the party leader { "party_id": "pty_41c0", "mode": "duo_fpp", "regions": ["eu-central", "eu-west"], "ping_ms": { "eu-central": 31, "eu-west": 48 }, // measured by the client against beacons "client_build": "31.2.0.448" } // 202 Accepted; the result arrives later on the party WebSocket { "ticket": "mm_9a72e1", "state": "searching" }
GETwss://party.example.net/v2/streamServer push for party and match events
// pushed when the allocator has a server ready { "type": "match_assigned", "match_id": "m_2026_eu_c_77310", "endpoint": "203.0.113.44:31206", "join_token": "v1.Qm9vdHMuLi4", // signed, single use, expires in 60 s "map": "desert_8k" }
POST/internal/matches/{match_id}/summaryGame server reports the final result
// mTLS between game server and results service; retried until acknowledged { "ended_at": "2026-10-01T18:42:09Z", "placements": [ { "team": 17, "rank": 1, "players": ["u_118", "u_903"] } ], "replay_uri": "s3://replays-eu/2026/10/01/m_2026_eu_c_77310.rpl", "summary_hash": "sha256:5be1..." // idempotency, a duplicate post is a no-op }

The in-match UDP channel

Every datagram carries a small header that gives you reliability only where you need it. The sender numbers its packets, and the receiver echoes back the newest sequence it saw plus a 32-bit mask of the ones before it. From that mask each side knows which packets landed without ever retransmitting the ones that did not.

// header, 12 bytes conn_id u32 // assigned at handshake, survives NAT rebinding seq u16 ack u16 // newest packet received from the other side ack_bits u32 // bit i set means seq (ack - 1 - i) also arrived // client to server: input frame, the newest plus the previous 3 for loss cover tick u32 buttons u16 // fire, aim, crouch, jump, interact... move i8 x 2 yaw, pitch u16 x 2 // quantized view angles // server to client: snapshot tick u32 baseline u32 // the snapshot this one is a delta against last_input u32 // newest client input the server has applied entities [ id, changed-field mask, packed fields ] events reliable lane: kills, pickups, zone timers
Two lanes on one socket

State is sent unreliably, because a newer snapshot always replaces an older one. A handful of things must arrive exactly once, such as "you picked up the level 3 helmet" or chat. Those ride a reliable, ordered lane inside the same packets and are resent until acknowledged. You get TCP-like guarantees for the 5% of traffic that needs them and none of the stalls for the rest.

05

How the pieces fit

05

How the pieces fit

Platform, fleet and one match serverDiagram
flowchart LR
	C["Game client"] -->|HTTPS and WSS| GW["Platform gateway"]
	GW --> PT["Party and presence"]
	GW --> MM["Matchmaker"]
	MM --> AL["Fleet allocator"]
	AL --> FL["Regional host fleet"]
	FL --> GS["Match server process"]
	C <-->|UDP 30 Hz| RL["Relay or DDoS edge"]
	RL <--> GS
	GS -. "events" .-> K[["Kafka: match events"]]
	GS -. "replay file" .-> OS[("Object storage")]
	K --> ST["Stats and ranking"]
	K --> AC["Anti-cheat analysis"]
	ST --> DB[("Player DB")]
	CDN["CDN"] -->|patches| C

Read the diagram as two layers that touch in exactly one place. The platform layer on the left is a set of stateless services behind a gateway, deployed a few times a day, backed by a player database and Redis. The match layer on the right is a fleet of hosts, one per region, that run short-lived simulation processes. The allocator is the bridge: it takes a filled lobby from the matchmaker and returns the address of a process that is already warm and waiting.

Clients never connect to a match server's real address. Their UDP traffic goes through a relay edge that filters floods, hides host IPs, and can route over a private backbone when that beats the public internet. After the match, the server writes its replay to object storage, posts a signed summary, and streams per-event telemetry to Kafka, where stats, ranking and anti-cheat consume it independently.

Matchmaker

Groups tickets by region, mode and skill band, and widens the band the longer a ticket waits. It works in short rounds of about two seconds per region, so it is a batch job that runs constantly, not a request handler.

Fleet allocator

Keeps a pool of pre-started, idle match processes on every host, with the map already loaded. Assigning a match is a state flip from ready to allocated, which takes milliseconds instead of the 20 seconds a cold start needs.

Match server

One process per match, pinned to a couple of cores. It owns all game state in memory, has no database on its tick path, and exits when the match ends. Its only outbound calls are fire-and-forget telemetry and the final summary.

Post-match pipeline

Kafka decouples the 26,000 live servers from the slower consumers. If ranking is down for ten minutes, matches carry on and results simply land late. Nothing in the pipeline can block a tick.

06

Deep dive: making 100 players agree

06

Deep dive: making 100 players agree

Three illusions working together

No player ever sees the true current state. Your own character is predicted ahead of the server, everyone else is shown slightly in the past, and when you fire, the server rewinds the world to what you were looking at. Each technique hides latency in a different place, and each has a cost you should be able to name.

The server loop

The server does not react to packets as they arrive. It buffers them, and every 33.3 ms it runs one fixed step: apply each player's queued inputs in tick order, move vehicles and projectiles, resolve shots, tick the zone, then build and send a snapshot for each client. A fixed step makes the simulation deterministic for a given set of inputs, which is what lets clients run the same code for prediction and what makes replays possible.

One 33 ms server tickDiagram
flowchart TD
	A["Drain UDP socket into per-player input queues"] --> B["Apply inputs for this tick"]
	B --> C["Physics: players, vehicles, projectiles"]
	C --> D["Validate shots with rewound hitboxes"]
	D --> E["Zone damage, deaths, loot changes"]
	E --> F["Update spatial grid"]
	F --> G["Per client: pick relevant entities"]
	G --> H["Delta against acked baseline, quantize, send"]
	H --> I{"Time left in budget?"}
	I -->|yes| J["Sleep until next tick"]
	I -->|no| K["Record overrun, shed far updates next tick"]
StageTypical cost, 100 live playersNote
Receive and decode inputsabout 1.5 msbatch reads with recvmmsg
Movement and physicsabout 9 msvehicles dominate
Hit validationabout 3 msspikes in firefights
Game rules and zoneabout 2 mscheap and steady
Relevance and snapshot buildingabout 9 msruns once per client
Sendabout 2 msbatched sendmmsg
Headroomabout 6.8 mskept free for spikes

Prediction and reconciliation for your own player

When you press forward, the client does not wait. It applies the input locally using the same movement code the server runs, renders the result, and stores the input in a ring buffer tagged with its tick number. The input is also sent to the server.

Each snapshot that comes back says "this is where you were after input N". The client compares that with its own record for tick N. If they match, which is almost always, it drops older entries and carries on. If they differ, because you got hit by a vehicle you had not seen yet, the client snaps to the server's state and replays every stored input after N on top of it. Small errors are blended out over a few frames so the camera does not jerk. The player only ever notices a correction when something genuinely surprising happened.

Interpolation for everyone else

Other players cannot be predicted, because you do not have their inputs. Instead the client keeps the last few snapshots and renders remote entities at a point in time about two ticks behind the newest one, roughly 66 ms at 30 Hz, interpolating between the two snapshots that surround that moment. One lost packet then costs nothing visible, because there is still a later snapshot to blend towards. The price is that every enemy you see is slightly old, which is exactly what lag compensation has to correct.

Lag compensation: rewinding for the shot

How a shot is judgedDiagram
flowchart LR
	F["Client fires at tick 1042, interp delay 66 ms"] --> P["Packet reaches server at tick 1046"]
	P --> R["Server computes the view time the shooter saw"]
	R --> CAP{"Rewind under 200 ms?"}
	CAP -->|no| CL["Clamp to 200 ms"]
	CAP -->|yes| H["Restore hitboxes from history ring"]
	CL --> H
	H --> T["Trace the shot against old hitboxes"]
	T --> V["Apply damage in the present tick"]
	V --> N["Notify shooter and victim"]

The server keeps a short history of every player's hitbox positions, one entry per tick for the last 200 ms or so. When a fire command arrives, it knows the shooter's round trip and interpolation delay, so it can work out which moment the shooter was actually looking at. It temporarily moves every other player's hitboxes back to that moment, traces the shot, restores them, and applies any damage in the present.

This is what makes aiming at a running target work on a 90 ms connection. It also creates the most common complaint in shooters: "I was already behind the wall". From the victim's point of view they reached cover, but on the shooter's screen they were still exposed, and the server sided with the shooter. Capping the rewind window keeps the effect bounded: a player on a 400 ms connection has to lead their shots instead of dragging everyone else into the past.

Peeking favours the mover

The player who steps around a corner sees the defender before the defender sees them, because the defender's view of the peeker is delayed by the peeker's upload time, the server tick and the defender's interpolation. Higher tick rates and lower interpolation delay shrink the gap. Nothing removes it. Say this out loud in the interview; it shows you know which latency can be engineered away and which cannot.

Area of interest: who needs to know about whom

Sending all 100 players to all 100 clients every tick is 10,000 entity updates per tick before you count loot, vehicles and projectiles. It is also a gift to wallhack authors, since the client would know where everyone is. Instead the server divides the 8 km map into a grid of 250 m cells, 32 by 32, and keeps each entity registered in its current cell.

For each client it collects entities from nearby cells and assigns a priority: distance, whether the entity is in the view cone, whether it just fired, and how long since that client last heard about it. Close and important entities go every tick. Distant ones go every third or fifth tick. Someone a kilometre away who is not shooting may not be sent at all. The packet has a byte budget, and the priority queue fills it until it is full. A scoped rifle extends the relevance range in the aimed direction so snipers still see their targets.

Delta compression and quantization

Each client acknowledges snapshots through the packet header, so the server knows the newest state that client definitely holds. The next snapshot encodes only what changed since that baseline: a bitmask per entity saying which fields follow, then the fields themselves. Positions are quantized to centimetres inside the map's bounds, angles to 16 bits, health to a byte. Together, relevance filtering, deltas and quantization take a naive 5 KB per client per tick down to the 600 bytes or so used in the estimates. If a client stops acking for a while, the server falls back to a full state for it rather than deltas against something the client may never have received.

07

Choices and their price

07

Choices and their price

Chosen
Authoritative dedicated server

The server decides every position, hit and pickup, and clients only send inputs. Cheats that edit memory to give infinite ammo or teleport simply have no effect. The cost is thousands of hosts and a CPU bill that grows with tick rate.

Required for competitive play
Rejected
Client authority or peer-to-peer

Cheap to run and fine for co-op games among friends. With 100 strangers, one host's connection decides the experience for everyone, and any client can lie about its own state. Hit detection trusted to clients is the quickest way to an aimbot epidemic.

Not for 100-player PvP
Chosen
30 Hz, with 60 Hz for the late game

At 30 Hz a full lobby fits the tick budget on two cores. Once the player count drops below about 40, the server can double its rate for the final circles, where fights are closest and precision matters most, at no extra hardware cost.

Matches the load curve
Alternative
Fixed 60 or 128 Hz

Tactical shooters with 10 players run 64 to 128 Hz comfortably. Multiply that by ten players per match and 26,000 matches and you need several times the fleet and bandwidth, for a difference most players feel only in close fights.

For small lobbies
Chosen
UDP with selective reliability

Lost state is replaced by the next snapshot, so it never needs resending. Only events go on the reliable lane. A single lost datagram costs 33 ms of staleness at worst.

Industry standard
Rejected
TCP or WebSockets

One lost segment holds back every later byte until it is retransmitted, so a 1% loss rate turns into regular freezes of a full round trip or more. Fine for the lobby and chat, wrong for movement.

Platform traffic only
Chosen
Uniform grid for relevance

Players are spread fairly evenly over a fixed-size map, so a flat grid gives constant-time inserts and neighbour lookups with no rebalancing. It is about forty lines of code and trivially cache friendly.

Fits a bounded map
Alternative
Quadtree or octree

Adapts to dense hot spots, which helps when 30 players land in one town. It costs rebalancing work every tick as everyone moves. Worth it for open worlds with uneven density, rarely worth it here.

If profiling demands it
Chosen
Warm pools on VMs, bare metal for the base

Own or reserve bare metal for the predictable daily baseline, and burst into cloud VMs in the same regions for weekends and launches. Both run the same container image with pre-started idle processes.

Cost plus elasticity
Cost of the alternative
All cloud, on demand

Simplest to operate. At 375 Gbps of sustained egress, cloud data transfer pricing becomes one of the largest lines in the budget, and noisy neighbours cause tick jitter you cannot see or fix.

Good for launch, costly later
Chosen
Server checks plus a client anti-cheat agent

The server already rejects impossible movement and limits what it sends. A client agent catches what the server cannot see, such as screen-reading aimbots and memory readers, and offline analysis of telemetry finds statistical outliers.

Layered defence
Trade-off
Kernel-level driver

Harder to evade, but it runs with full system privileges on players' machines, raises real privacy and stability concerns, and is unavailable on some platforms. Many studios now use it; you should be able to argue either side.

Product decision

08

Failure modes

08

Failure modes

💥
A match server crashes at minute 22

A rare combination, say a vehicle exploding on a zone edge during a revive, hits a null pointer. Up to 100 players are dropped from a match they have invested twenty minutes in, and support tickets follow.

Crash dumps go straight to triage with the replay attached, so the exact inputs can reproduce the bug. Ship server builds to 1% of matches first and watch crash rate per build. Compensate affected players automatically. Mid-match failover sounds attractive but is rarely worth its complexity; prevention is cheaper.
⏱️
Ticks overrun in a hot drop

Forty players land in the same town, grenades and vehicle physics pile up, and the tick takes 45 ms instead of 33. The server falls behind real time and every client sees rubber-banding.

Measure tick time per stage and degrade gracefully: send far entities less often, simplify physics for vehicles nobody is near, cap projectiles. Keep the host packing conservative so one heavy match does not starve its neighbours on the same machine.
📶
Loss bursts on a player's connection

A player on a busy Wi-Fi network loses 15% of packets for three seconds. Their inputs arrive in clumps and remote players freeze and jump on their screen.

Repeat recent inputs in every packet so a single loss is covered, let the server extrapolate a missing input briefly using the last one, and widen the client's interpolation buffer when it measures jitter. Show a network indicator so the player knows the problem is local.
🌊
Patch day: everyone logs in at once

A new season goes live and peak concurrency jumps 60% in ten minutes. The login service and matchmaker are swamped, and the fleet runs out of warm processes.

Pre-scale the fleet the day before, put a login queue with an honest position counter in front of authentication, and let the matchmaker shed load by pausing low-priority modes. Patch downloads come from the CDN, never from origin.
🛡️
A volumetric DDoS on a game host

Someone who lost a ranked match floods the server address. Every match on that host lags out, which is exactly what the attacker wanted.

Never expose host IPs: clients talk to relay edges with scrubbing capacity, and a match can be moved to a different relay without changing the server. Validate the connection id on the first bytes of every packet and drop the rest cheaply.
🎯
Cheats that the server cannot see

An aimbot reads the screen and moves the mouse. Its inputs are perfectly legal, just inhumanly accurate, so server validation passes them.

Stream aim telemetry to Kafka and score it offline: reaction times, snap angles, hit ratios through smoke. Flag outliers for review using the server replay, and ban in waves so cheat makers cannot tell which signal caught them.
🧩
Client and server builds disagree

A hotfix changes recoil on the server, but some clients still run the old build. Prediction diverges on every shot and players see constant corrections.

Carry the build id in the matchmaking ticket and in the UDP handshake. The matchmaker only places a ticket on servers of the same protocol version, and the server refuses mismatched clients with a clear update prompt.

09

How to handle the interview

09

How to handle the interview

1

Split the problem in two at the start. Say that platform services are a familiar web system and the match server is a real-time simulation, then spend most of your time on the second. Interviewers want to see that you know where the difficulty lives.

2

State the tick budget as a number. "30 Hz means 33 ms for everything" frames every later decision, from the relevance grid to how many matches fit on a host.

3

Explain prediction, interpolation and lag compensation as one story. Each hides latency somewhere different. Walk through a single shot from key press to damage, naming which clock each step uses.

4

Justify UDP with a failure, not a slogan. Describe what head-of-line blocking does to a movement stream under 1% loss, and then show that you still need a small reliable lane for events.

5

Do the bandwidth sum. Naive full-state broadcast is enormous; relevance, deltas and quantization bring it into a range a home connection handles. Showing the before and after is more convincing than naming the techniques.

6

Talk about the fleet, not just one server. Warm pools, packing by peak rather than average, and region selection by measured ping are what make "a server per match" work at 26,000 matches.

7

Treat cheating as a design input. Server authority, limited information to clients, and offline telemetry analysis each stop a different class of cheat. Mention the privacy trade-off of kernel drivers if asked.

8

Admit what you would not build. Mid-match server failover is a common trap. Saying it is too complex for its payoff, and that you would invest in crash prevention instead, reads as judgement.

10

From prototype to global fleet

11

From prototype to global fleet

Stage 1: closed alpha, a few thousand players
Hand-run servers and a simple queue

A handful of hosts in one region, a matchmaker that fills lobbies first come first served, and a config file listing server addresses. Full-state snapshots are fine because nobody is measuring bandwidth yet. The goal is to find out whether the game is fun.

Stage 2: early access, tens of thousands concurrent
Netcode gets serious

Delta compression, the relevance grid and lag compensation land because bandwidth and fairness complaints demand them. Results go to a stats database through a queue. Anti-cheat starts with server-side movement checks.

Stage 3: launch, hundreds of thousands concurrent
Orchestrated fleets in several regions

A fleet allocator with warm process pools replaces the static list, ping beacons let clients pick regions, and skill-based matchmaking with widening bands arrives. Relay edges go in front of servers after the first DDoS.

Stage 4: millions concurrent
Hybrid capacity and a telemetry pipeline

Reserved bare metal carries the baseline, cloud bursts cover peaks, and Kafka fans match events out to ranking, anti-cheat models and analytics. Server replays become the evidence store for bans and disputes.

Stage 5: mature live service
Variable tick rate and smarter routing

The server raises its tick rate as players die, relay networks route over a private backbone, and capacity planning is driven by forecasts of each season's launch. The architecture barely changes from here; tuning does.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.