Realtime Data Synchronization

Written by Backbone Tutorials Team

Last updated: June 2026 · 10 min read

Opening a WebSocket is the easy part. The harder problem starts the moment two clients both have a copy of some state and one of them changes it. How does the other client find out? How does it apply the change without corrupting what it already has? What happens when the network drops and a client misses several updates? Realtime synchronization is the engineering discipline that answers those questions, and getting it right is the difference between a collaborative app that feels solid and one that silently diverges.

This guide follows the transport guides, WebSocket, Server-Sent Events, and WebRTC data channels, and provides the state-management layer that sits on top of any of them.

Sync message flow with sequence numbers Server broadcasts delta updates tagged with sequence numbers; clients apply updates in order and request catch-up when a gap is detected. Server authoritative state + log Client A seq=42 (current) Client B seq=40 (missed 41,42) Client C reconnecting → catch-up catch-up req seq=39
The server broadcasts delta updates tagged with sequence numbers. Clients apply in order; a gap triggers a catch-up request.

Full-state broadcast vs delta updates

The first decision in any sync system is how much to send when something changes. The two extremes each have clear trade-offs.

Choosing the right payload size

// Full-state broadcast: simple to apply, expensive for large state
ws.send(JSON.stringify({ type: 'state', data: fullDocument }));

// Delta update: cheap on the network, requires careful application
ws.send(JSON.stringify({
  type: 'delta',
  seq: 43,
  op: 'set',
  path: '/title',
  value: 'New Title'
}));

Full-state broadcast sends the complete current state every time anything changes. Every client can simply replace what it has with what it receives, requiring no merge logic. This works well when state is small, say a shared counter or a short status object, and becomes impractical when state is large. A collaborative document with thousands of nodes cannot afford to broadcast the full tree for every keystroke. Delta updates, describing only what changed, keep payloads small at the cost of requiring the client to apply patches correctly and in order. The right choice is usually a hybrid: deltas for live updates, periodic or on-demand full snapshots as a reset anchor.

Sequence numbers and ordering

A stream of updates without ordering guarantees is a source of silent data corruption. Sequence numbers are the minimal structure needed to prevent it.

Detecting gaps and ordering messages

var expectedSeq = 0;
var pendingBuffer = [];

function onMessage(msg) {
  if (msg.seq === expectedSeq) {
    applyUpdate(msg);
    expectedSeq++;
    // flush any buffered messages that are now in order
    while (pendingBuffer.length && pendingBuffer[0].seq === expectedSeq) {
      applyUpdate(pendingBuffer.shift());
      expectedSeq++;
    }
  } else if (msg.seq > expectedSeq) {
    pendingBuffer.push(msg);
    pendingBuffer.sort((a, b) => a.seq - b.seq);
    requestCatchUp(expectedSeq); // ask server to fill the gap
  }
  // seq < expectedSeq: duplicate, discard silently
}

Each update carries a monotonically increasing sequence number from the server. The client applies updates in order, buffers anything that arrives early, and discards anything whose sequence it has already processed. A gap, receiving seq 45 when expecting 43, means updates 43 and 44 were lost in transit; the client requests them explicitly rather than silently skipping forward. This pattern prevents the most common failure mode in realtime systems, a client that processes events out of order and diverges from the authoritative state without knowing it.

Client-side merge strategies

When deltas arrive, the client must integrate them into its local state. The strategy depends on how the state is structured.

Last-write-wins and path-based merging

function applyDelta(state, delta) {
  switch (delta.op) {
    case 'set':
      return setPath(state, delta.path, delta.value);
    case 'delete':
      return deletePath(state, delta.path);
    case 'append':
      var arr = getPath(state, delta.path) || [];
      return setPath(state, delta.path, arr.concat(delta.value));
  }
  return state;
}

// Backbone: trigger model update from server delta
function syncDelta(model, delta) {
  var update = {};
  update[delta.field] = delta.value;
  model.set(update);
}

Last-write-wins by sequence number is the simplest valid strategy: the most recently sequenced value for a field wins. It works for independent fields that different users rarely edit simultaneously. For text or nested structures where two users might edit the same region, last-write-wins causes one person's work to silently overwrite another's, which is why collaborative documents need the operational transform or CRDT strategies covered in later guides. The Backbone pattern is clean here: each delta maps directly to a model.set call, and Backbone's event system propagates the change to views automatically.

Catch-up on reconnect

Clients disconnect. The key engineering question is not whether they will miss updates but how they recover when they do.

Replay log and snapshot fallback

// Client reconnects and announces its last known sequence
ws.addEventListener('open', function () {
  ws.send(JSON.stringify({
    type: 'catchup',
    lastSeq: localState.seq
  }));
});

// Server handler: replay or send full snapshot
function handleCatchup(client, lastSeq) {
  var logStart = eventLog.oldestSeq();
  if (lastSeq >= logStart) {
    eventLog.since(lastSeq).forEach(e => client.send(e));
  } else {
    // gap too large: send full snapshot
    client.send({ type: 'snapshot', seq: currentSeq, data: fullState });
  }
}

The server keeps a short-lived event log, typically the last few minutes of updates. On reconnect, the client sends its last known sequence number. If that falls within the log window, the server replays the missing range and the client catches up without the user noticing anything. If the gap is too large, the server sends a full snapshot and the client resets from it. The snapshot is the safety net; the log replay is the common case. Log window length is an operational knob: longer windows use more server memory but handle longer disconnections gracefully.

Idempotent message handling

Networks deliver duplicates. A robust sync client handles the same message arriving twice without applying it twice.

Tracking applied sequences

var appliedSeqs = new Set();

function applyUpdate(msg) {
  if (appliedSeqs.has(msg.seq)) return; // duplicate, skip
  appliedSeqs.add(msg.seq);
  // prune old entries to keep memory bounded
  if (appliedSeqs.size > 1000) {
    var oldest = Math.min(...appliedSeqs);
    appliedSeqs.delete(oldest);
  }
  applyDelta(localState, msg.delta);
}

The sequence number itself provides idempotency when the client tracks which numbers it has already applied. A set of recent sequence numbers is enough; anything older than the current window can be pruned. This also handles the case where a catch-up replay overlaps with messages the client had already received live. The client simply discards the duplicates and the state stays consistent.

Sync architecture patterns

The mechanics above combine into a small set of architectural patterns that cover most realtime applications.

Authoritative server, eventual consistency, and bounded divergence

The authoritative server pattern keeps a single source of truth on the server. Clients send intents; the server applies them, assigns sequence numbers, and broadcasts the results. Clients never apply changes directly to authoritative state; they optimistically show them locally and reconcile when the server confirms. This is the optimistic UI pattern. Eventual consistency accepts that clients may briefly diverge and relies on the sync protocol to converge them. Bounded divergence adds a maximum tolerable lag; a client that falls too far behind requests a snapshot rather than trying to catch up incrementally. Together these three ideas, an authoritative server, an event log, and snapshot fallback, form the backbone of every collaborative and multiplayer system from Google Docs to Figma to online games.

Frequently Asked Questions

What is realtime data synchronization?

Realtime data synchronization is the engineering practice of keeping the state on multiple clients consistent with the server and with each other as changes happen. It involves choosing how to broadcast changes, ordering and de-duplicating messages, and handling gaps that appear when a client disconnects and reconnects.

What is the difference between full-state broadcast and delta updates?

Full-state broadcast sends the complete current state on every change, which is simple to apply on the client but expensive on the network for large state. Delta updates send only what changed, which is cheaper but requires the client to apply the patch correctly and handle out-of-order or missing updates.

Why do sync messages need sequence numbers?

Sequence numbers let the client detect gaps, reorder out-of-order messages, and discard duplicates. Without them, a client that misses update 5 and receives update 6 cannot know it is behind and will silently diverge from the correct state.

How do you handle catch-up when a client reconnects?

On reconnect, the client sends its last known sequence number. The server replays all updates since that number from a short-lived log, then switches to live updates. If the gap is too large, the server sends a full-state snapshot instead and the client resets from it.

Read next: the Interactive Web hub, or continue to WebSocket fundamentals for the transport this sync layer rides on.

Want models that stay in sync with a live server?

The Backbone guide shows how models and collections integrate with realtime data sources.

Explore the Backbone Guide →