SYSTEM_DESIGN

System Design: Game State Synchronization

Design a robust game state synchronization system for multiplayer games that handles network jitter, packet loss, and client-server reconciliation using techniques like delta compression, client-side prediction, and rollback netcode.

16 min readUpdated Jan 15, 2025
system-designgamingnetcodelag-compensationwebsocketsgame-loop

Requirements

Functional Requirements:

  • Synchronize game world state between an authoritative server and 2-64 clients in real time
  • Handle client-side prediction: clients simulate movement locally and reconcile with server confirmations
  • Rollback netcode: on prediction mismatch, roll back local state and replay from the last confirmed server state
  • Delta compression: transmit only changed state fields to minimize bandwidth
  • Interest management: send each client only the state of entities within their visible area
  • Support multiple transport protocols: UDP for low-latency games, WebSocket/WebTransport for browser games

Non-Functional Requirements:

  • Acceptable gameplay at up to 150ms RTT with lag compensation
  • Bandwidth per client under 50 Kbps for typical state updates
  • State divergence between clients should be under 1 game tick at 99th percentile
  • Tick rate configurable from 10 Hz (strategy) to 128 Hz (competitive FPS)
  • Handle 5% packet loss without visible game impact via redundancy and interpolation

Scale Estimation

At 64 Hz tick rate with 64 players, the server processes 64 × 64 = 4,096 input packets/second per session and broadcasts 64 × 64 = 4,096 state update packets/second per session. At 500 bytes per state delta (compressed), that's 2 MB/second outbound per session. A game server hosting 100 sessions needs 200 MB/second = 1.6 Gbps outbound, requiring a 10 Gbps NIC. For 100k concurrent sessions globally, total bandwidth is ~200 TB/hour of state sync data — entirely handled by game server fleet egress, not CDN.

High-Level Architecture

The synchronization model is client-server with a single authoritative server per game session. Clients never trust each other's state — all game state changes are validated by the server. The client runs a local simulation for responsiveness but treats it as a prediction that may be corrected by server updates.

The server game loop runs at fixed tick intervals. Each tick: (1) receive and queue all client input packets that arrived since last tick, (2) apply inputs to the authoritative game world state, (3) detect any anti-cheat violations in the applied inputs, (4) compute per-client state delta using interest management (which entities each client can see), (5) serialize and send delta packets to each client. Client inputs are timestamped with the client's local tick number; the server maps client tick numbers to server ticks using the client's estimated clock offset (measured via NTP-like ping/pong handshake at session start).

The client runs its own local simulation at the same tick rate. When the player presses a movement key, the client immediately applies the movement to the local state (client-side prediction) and sends the input to the server. When the server's confirmed state packet arrives, the client compares the server's confirmed position for tick T with what the client predicted for tick T. If they match, no correction needed. If they diverge (misprediction), the client rolls back to tick T's server state and re-applies all inputs from T to the current tick — "rollback and replay."

Core Components

Input Processing and Timestamping

Client inputs are serialized with: client_tick (the client's local simulation tick when the input was generated), input_flags (bitmask of pressed keys/buttons), analog_inputs (joystick axes as fixed-point values), and a sequence number. The server accumulates inputs per client in a jitter buffer sized to 2× the client's measured jitter (standard deviation of inter-packet arrival times). This absorbs network jitter and provides a stable stream of inputs to the game loop. If the jitter buffer empties (packet loss), the server extrapolates the last known input state for up to 3 ticks (150ms at 20 Hz) before treating the input as neutral (no input).

Delta Compression Engine

Delta compression compares the current tick's world state against the last state acknowledged by each client. Only changed fields are included in the update packet. Implementation: each entity in the world state has a snapshot buffer of the last 64 ticks' states. For each client, track the last acknowledged snapshot tick. The delta is computed as a bitmask of changed fields plus the new values, serialized in a compact binary format (not JSON). Field-level delta compression: a position change of 1cm in a game world is serialized as a 12-bit fixed-point delta rather than a full 64-bit float — 5× bandwidth reduction for typical movement updates. The server also marks redundantly transmits the 3 most recent state snapshots to handle packet loss (the client applies the most recent one it hasn't seen yet).

Interest Management System

Interest management limits each client to receiving only the state of entities within a configurable radius (or within the same zone/room for dungeon-style games). The server maintains a spatial index (grid or BVH tree) of all entities. Per tick, for each client, a range query retrieves entities within the interest radius. Entities outside the radius are not included in the delta packet. This reduces bandwidth proportional to world size — in a large open world, a client with 100m view distance receives state for ~100 entities out of potentially 10,000 in the world, reducing bandwidth by 99×. Newly entering entities (entering interest radius) receive a full snapshot; leaving entities receive a "despawn" notification.

Database Design

Game state synchronization is primarily an in-memory and network concern; database usage is limited to session metadata and post-match analytics. Redis: session:{session_id}:state (latest authoritative world state snapshot, binary blob), session:{session_id}:client:{player_id}:ack_tick (last acknowledged tick per client, for delta computation), session:{session_id}:tick (current server tick counter). PostgreSQL: game_sessions (session_id, game_mode, region, started_at, player_ids[], tick_rate, transport_protocol, ended_at), session_metrics (session_id, avg_tick_jitter_ms, avg_packet_loss_pct, max_players, peak_bandwidth_mbps) — post-session analytics. No real-time DB writes during active sessions — all in Redis/in-memory.

API Design

  • UDP socket or WebSocket /game/{session_id}?token={t} — bidirectional binary protocol; client sends input packets (fixed binary format), server sends state delta packets; framing uses 2-byte length prefix
  • POST /sessions/{session_id}/connect — REST pre-connect: validates token, returns UDP/WebSocket endpoint and session configuration (tick_rate, world_size, interest_radius)
  • GET /sessions/{session_id}/state/snapshot — REST endpoint for initial full-world state download on join; only called once at session join, not per-tick
  • WebTransport /game/{session_id} — QUIC-based alternative for browser clients; separate reliable stream for critical events (spawn/despawn) and unreliable datagrams for state updates

Scaling & Bottlenecks

CPU is the primary bottleneck. At 128 Hz, the server has 7.8ms per tick to process all inputs and compute deltas for 64 clients. Delta computation is O(entities × clients) per tick — for 1,000 entities and 64 clients, that's 64,000 entity-client pairs evaluated per tick. Using SIMD-optimized diff routines and pre-computed entity visibility masks (updated at 10 Hz, not per-tick) reduces delta computation to ~1ms. The remaining 6ms budget handles input processing, physics simulation, and serialization.

Network I/O is the secondary bottleneck. Sending 64 individual UDP packets per tick (one per client) at 128 Hz = 8,192 UDP sends/second. Linux kernel UDP send syscalls at this rate saturate a single CPU core — use sendmmsg() (batch UDP send) to send all 64 packets in a single syscall, reducing syscall overhead by 64×. For WebSocket clients, use write batching with Nagle disabled (TCP_NODELAY) to prevent 200ms Nagle buffering delay.

Key Trade-offs

  • Rollback netcode vs. delay-based netcode: Rollback (GGPO-style) provides the most responsive feel for fighting games and fast-paced games but requires game state to be fully serializable and replayable; delay-based netcode is simpler (add fixed input delay, no rollback) but feels sluggish on high-latency connections. Rollback is worth the complexity for competitive multiplayer.
  • UDP vs. WebSocket: UDP with a custom reliability layer (selective ACK, redundant sends) provides the lowest latency and most control over packet delivery semantics; WebSocket (TCP) adds head-of-line blocking (one lost packet stalls all subsequent packets) making it unsuitable for 64 Hz+ tick rates. WebTransport (QUIC) solves HOL blocking in browsers.
  • Interest management radius: A larger radius improves game awareness for players but increases bandwidth; a smaller radius reduces bandwidth but causes "pop-in" (entities suddenly appearing as they enter the radius). Dynamic radius (adjust based on game speed and player count) balances these.
  • Tick rate vs. computational cost: Higher tick rates (128 Hz) reduce input latency and improve game feel but double the computational load compared to 64 Hz; for most game genres, 20-30 Hz is sufficient, reserving 64-128 Hz for competitive FPS titles where 1-frame input latency matters.

GO DEEPER

Master this topic in our 12-week cohort

Our Advanced System Design cohort covers this and 11 other deep-dive topics with live sessions, assignments, and expert feedback.