The 20 Hz Server Tick
The Nakama match handler runs at 20 Hz — one tick every 50 milliseconds. This rate was chosen to balance claim-resolution responsiveness against server CPU cost for a turn-like real-time card game, where 50 ms latency is imperceptible to players. Each tick performs work in strict order:Full State vs. Delta Updates
A key design decision: the server sends the completeMatchState every tick, not a diff of what changed.
Why full-state?
- Dropped messages are self-healing. If a client misses one broadcast, the next broadcast corrects it automatically. There is no dependency on receiving every packet in sequence.
- No delta-reconstruction bugs. Delta protocols require perfectly ordered delivery and a consistent baseline. When packets are dropped or reordered on mobile networks, delta reconstruction fails silently and produces corrupted state that is extremely hard to debug.
- State is small enough. A complete
MatchStatesnapshot for a SET game is approximately 500 bytes — well within WebSocket message budgets even at 20 Hz.
ApplyServerState() is the only write path for multiplayer game state on the client.
MatchState Broadcast Contents
Every 20 Hz broadcast includes the full picture needed to render the game correctly:
The client tracks the last applied
seq. If a broadcast arrives with a lower seq than the last applied state, it is discarded — this prevents a delayed packet from overwriting a more recent state.
Client Mirror Model
In multiplayer, the client’sGameSession is a read-only mirror of the server state. It does not simulate game logic independently. The OnlineMatchController is the bridge between NakamaMultiplayerService and GameSession: it subscribes to IMultiplayerService.Messages, translates each ServerMessage into an ApplyServerStateCommand, and dispatches it to GameSession.
Consequences of this model:
- Even if the client drops three consecutive broadcasts, the fourth corrects it completely — no manual “catch-up” logic required.
- The client must not run
SetValidatoror update scores locally in multiplayer. Any local state computation creates a divergence that will conflict with the nextApplyServerStatecall. - All animations and VFX are triggered by the reactive stream reacting to
ApplyServerState, not by theSendClaimcall itself.
Latency Compensation
The server provides an immediate acknowledgement when a claim message is received — before the claim is processed in the next tick:OnlineMatchController uses the ACK to show a “claiming…” visual state immediately, without waiting for the full tick to resolve. The worst-case delay for a confirmed result is one tick period (50 ms) + network RTT. For a game with p95 RTT of 150 ms, that means approximately 200 ms from tap to confirmed feedback — which is imperceptible as latency in a turn-like card game.
The latency thresholds enforced in the client are:
These are Hard Boundaries and must not be changed without a formal change request.
Reconnection Flow
When a client loses its WebSocket connection, the server starts a 30-second reconnect window. During this window, the match continues for all other players. The disconnected player’s session is held open on the server.Reconnect Rules
What Happens to State During Disconnect
The client must not attempt to continue simulating game state while disconnected. Input should be disabled. TheGameSession stays in a Disconnected sub-state. When reconnection succeeds, ApplyServerState replaces everything — any local speculation would be overwritten anyway, and speculative state creates confusing UI artefacts.
Online Pause Behaviour
Online pause is not permitted in multiplayer matches. This is a Hard Boundary. The match timer continues running during any single player’s disconnect. The reconnect window countdown is a server-side timeout, not a game pause. All other players continue playing at full speed. This prevents a player from gaining an advantage by deliberately disconnecting to “pause” a losing position. If you are implementing the pause system, thePauseCommand must be gated by IOnlineGameSession.IsMultiplayer — if true, the pause request is silently ignored or disabled in the UI.
Implementation Checklist
- Client disables all input immediately on
DisconnectedEvent— do not allow speculative play while disconnected -
ApplyServerState()is the single write path for all multiplayer game state; no other code path modifiesGameSessionfields in multiplayer mode -
OnlineMatchControllersubscribes toIMultiplayerService.Messagesand translates eachServerMessageinto anIGameCommanddispatched toGameSession - Reconnect uses the same match ID and session token — do not generate a new session, as the server uses the token to restore the existing player slot
- The 30-second countdown is shown as a visible timer, not just a spinner — players need to know how long they have
- After reconnect, all client state is correct without any additional “sync” calls —
ApplyServerStateon the full snapshot is sufficient -
seqtracking discards stale messages that arrive out of order after a reconnect - Online pause requests are blocked at the UI layer — the pause button should not appear or be tappable in multiplayer
Common Mistakes
Related Pages
Authority Model
Why the server validates every Set, and the interface contracts that enforce it.
Match Lifecycle
The full flow from matchmaking queue to post-match results, including the in-match play loop.
Anti-Cheat
Rate limiting, server-side data ownership, and why the client cannot influence outcomes.
Session Lifecycle
How GameSession manages state transitions across all three game modes.