Multiplayer State Synchronization
A shared whiteboard where ten people draw at once, a design tool where teammates move objects together, a live dashboard where metrics update for every viewer at the same moment: these are all multiplayer state problems. Unlike collaborative text editing, where the challenge is merging concurrent changes to a document, multiplayer state synchronization is primarily about keeping a shared world consistent and smooth across many clients over a network where every message arrives late. The latency is not a bug to fix; it is a physical constant to engineer around.
This guide sits at the practical end of the Interactive Web cluster, drawing on the sync mechanics from realtime data synchronization and the optimistic patterns from optimistic UI updates.
What you'll learn
The authoritative server model
In any shared-state system where the outcome matters, one machine must be the final arbiter. That machine is the authoritative server.
Server owns truth, clients own display
The server holds the canonical state of the shared world. Clients send inputs, never state mutations. The server validates every input, applies it according to its own physics or business rules, and broadcasts the resulting state to all connected clients. A client cannot update shared state by modifying its local copy; it can only influence it by sending a valid input and waiting for the server to accept it. This means one malformed or adversarial client cannot corrupt the experience for others. The pattern from realtime sync applies directly: the server is the authority, and clients are eventually consistent views of it.
Client-side prediction
If a client waits for the server to confirm every input before showing a result, the interface feels unresponsive on any non-zero latency connection.
Apply locally, reconcile on confirmation
var localState = {};
var pendingInputs = [];
function applyInput(input) {
pendingInputs.push(input);
// Apply locally without waiting for server
localState = simulate(localState, input);
render(localState);
ws.send(JSON.stringify({ type: 'input', seq: input.seq, data: input.data }));
}
function onServerState(msg) {
// Server confirmed up to msg.lastSeq
pendingInputs = pendingInputs.filter(function (i) { return i.seq > msg.lastSeq; });
// Rebase: start from authoritative state, replay unconfirmed inputs
var state = msg.state;
pendingInputs.forEach(function (i) { state = simulate(state, i); });
localState = state;
render(localState);
}
The client applies its own input immediately and continues rendering. Unconfirmed inputs are kept in a pending queue. When the server responds with its authoritative state and the last sequence it processed, the client discards confirmed inputs, resets to the server state, and replays any remaining pending inputs on top. If the server and client agree, the rendered result is unchanged. If they diverge, the client snaps to the correct state. The optimistic UI pattern is the same mechanism applied to discrete operations rather than continuous simulation.
Input queues and sequence numbers
Inputs arrive at the server in network order, which may not match the order the client generated them. Sequence numbers let the server reconstruct intent.
Stamped inputs and server ordering
var inputSeq = 0;
function sendInput(data) {
inputSeq++;
var input = {
seq: inputSeq,
timestamp: Date.now(),
data: data
};
pendingInputs.push(input);
ws.send(JSON.stringify({ type: 'input', input: input }));
}
// Server: buffer inputs, process in order
var inputBuffer = [];
function handleInput(clientId, input) {
inputBuffer.push({ clientId, input });
inputBuffer.sort(function (a, b) { return a.input.seq - b.input.seq; });
processNextInputs();
}
Each input carries a monotonically increasing sequence number from the client, matching the pattern from the sync guide. The server buffers inputs that arrive out of order and processes them in sequence order. A small buffer window, typically fifty to one hundred milliseconds, handles reordering without stalling. Inputs older than the window are dropped; the client's reconciliation loop handles the resulting correction automatically.
State interpolation for remote entities
State updates from remote entities arrive at intervals. Rendering them only at update time produces jerky movement. Interpolation smooths the path between received states.
Linear interpolation between state snapshots
var remoteBuffer = []; // last two received states for each entity
function onRemoteState(msg) {
remoteBuffer.push({ state: msg.state, t: msg.serverTime });
if (remoteBuffer.length > 2) remoteBuffer.shift();
}
function renderFrame(now) {
var renderTime = now - 100; // render 100ms behind to have buffer
if (remoteBuffer.length < 2) return;
var prev = remoteBuffer[0];
var next = remoteBuffer[1];
var alpha = (renderTime - prev.t) / (next.t - prev.t);
alpha = Math.max(0, Math.min(1, alpha));
var x = prev.state.x + (next.state.x - prev.state.x) * alpha;
var y = prev.state.y + (next.state.y - prev.state.y) * alpha;
drawEntity(x, y);
requestAnimationFrame(renderFrame);
}
The client renders one hundred milliseconds behind real time, maintaining a buffer of at least two received state snapshots. Linear interpolation between the two surrounding snapshots produces smooth movement at the full frame rate regardless of the update interval. The trade-off is a one hundred millisecond visual delay for remote entities, which is imperceptible in most collaborative tools and acceptable even in fast-paced interactive applications.
Dead reckoning
Interpolation requires two known states. When updates stop arriving because of a packet loss or a slow network, extrapolation keeps remote entities moving plausibly rather than freezing.
Project forward from last known velocity
function extrapolate(lastState, dt) {
// Project position forward using last known velocity
return {
x: lastState.x + lastState.vx * dt,
y: lastState.y + lastState.vy * dt,
vx: lastState.vx,
vy: lastState.vy
};
}
function renderFrame(now) {
var dt = (now - lastUpdateTime) / 1000; // seconds since last update
if (dt < 0.5) {
// Less than 500ms: extrapolate
var projected = extrapolate(lastKnownState, dt);
drawEntity(projected.x, projected.y);
} else {
// Too long: freeze, wait for next update
drawEntity(lastKnownState.x, lastKnownState.y);
}
requestAnimationFrame(renderFrame);
}
Dead reckoning projects position forward using the last known velocity. It works well for entities with stable, predictable movement and degrades gracefully when the prediction becomes wrong: the next real state update corrects the position, usually with a small snap that is less disruptive than a freeze. Capping the extrapolation window, five hundred milliseconds in the example, prevents wild divergence from accumulating before a correction arrives.
Lag compensation
Even with prediction and interpolation, network latency creates an asymmetry: the client acts on a visual state that is slightly in the past relative to the server.
Server rewind for fair evaluation
Lag compensation addresses this by having the server rewind its historical state to the moment a client's input was generated before evaluating it. The server keeps a rolling history buffer, typically one to two seconds of snapshots. When an input arrives tagged with the client's timestamp or the server tick it was generated against, the server rolls back to that state, evaluates the input against it, and then fast-forwards. The result is that a client action which was spatially correct at the time it was taken is judged correctly by the server, even though the action arrived late. This technique originated in realtime multiplayer networked applications and now applies broadly to any interactive web tool where fairness under latency matters: shared drawing boards, multiplayer design tools, and live map annotation systems. The latency and reconciliation guide examines the measurement and tuning side of these techniques.
Frequently Asked Questions
What is the authoritative server model?
In the authoritative server model, the server is the single source of truth for shared state. Clients send inputs and receive state updates; they never update shared state directly. The server validates every input, applies it, and broadcasts the result. This prevents cheating and ensures all clients converge to the same state.
What is client-side prediction?
Client-side prediction applies the local player's input immediately on the client without waiting for server confirmation. This removes the perceived latency of a round trip. When the server's authoritative state arrives, the client reconciles its predicted state with the server state, replaying any unconfirmed inputs on top.
What is dead reckoning?
Dead reckoning extrapolates the position of a remote entity by projecting its last known velocity and direction forward in time. It fills the gap between state updates so remote entities appear to move smoothly rather than jumping between received positions. The extrapolated position is corrected when the next update arrives.
What is lag compensation?
Lag compensation lets the server rewind its state to the moment a client's input was generated before processing it. This means a client action that was visually correct at the time it was taken is evaluated against the world state the client actually saw, not the state the server has by the time the input arrives.
Read next: the Interactive Web hub, or continue to latency and reconciliation for measuring and tuning the techniques introduced here.
Want a Backbone model that reflects authoritative server state?
The Backbone guide shows how models and collections handle live server updates cleanly.
Explore the Backbone Guide →