Real-Time

Real-time UX is a state-management problem before it's a networking problem

Opening the socket takes an afternoon. Deciding what the screen means when messages arrive out of order takes the rest of the project.

Every real-time project has a week where the socket connects, messages appear in the console, and the team believes the hard part is behind them. It is not. Transport was the part with documentation.

The four questions transport does not answer

  • What is authoritative — the last message received, or the last write this client made?
  • What does the interface show while those two disagree?
  • What happens to the optimistic update when the server rejects it?
  • After a reconnect, is the client resuming or starting over?

A product can be built with no explicit answer to any of these. It will work in development, where the network is a function call, and it will fail on a train.

Ordering is not guaranteed and users notice

Two updates to the same record, sent 40ms apart, can arrive in either order. If the interface renders whichever arrived last, the record will occasionally show the older value and stay that way until something else forces a redraw.

// Not: apply whatever arrived.
// Instead: apply only what is newer than what we already have.
function receive(update: Update, current: Record) {
  if (update.version <= current.version) return current; // late duplicate
  return { ...current, ...update.fields, version: update.version };
}
ts

Four lines. They are not clever, and they are the difference between an interface people trust and one they refresh out of habit.

Reconnection is a product decision

When a connection drops for eleven seconds, something happened that the client missed. There are exactly three honest options: replay the gap, refetch the world, or tell the user their view is stale. Choosing silently — which is what happens when nobody chooses — means picking the third and not telling them.

Reconnection strategies and what they cost
StrategyServer costCorrect when
Replay from cursorRetained event logOrder matters and history is bounded
Refetch snapshotOne expensive readState is small and latest-wins
Mark as staleNoneThe user must decide whether to trust it

All three are defensible. Only one of them is defensible by accident, and it is the one that makes the product feel unreliable.

Design the disagreement

The interesting states in a real-time product are the ones where the client and the server disagree: pending, superseded, rejected, stale, reconnecting. Those five states are the actual design work. The socket is plumbing.

Working on something this applies to?