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 };
} 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.
| Strategy | Server cost | Correct when |
|---|---|---|
| Replay from cursor | Retained event log | Order matters and history is bounded |
| Refetch snapshot | One expensive read | State is small and latest-wins |
| Mark as stale | None | The 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.
Related reading