Real-time
The WebSocket reconnect logic nobody writes
Real-time is easy on your laptop and hard on a driver's phone going through a tunnel. What I learned shipping live tracking and chat to people who move.

Every WebSocket tutorial ends at 'and now messages arrive instantly'. Which is true, on a desktop, on office wifi, with the tab focused. Ship the same code to a driver's phone in a moving vehicle and you'll discover how much of real-time is actually about what happens when the connection isn't there.
The connection drops constantly and never tells you
The first thing that surprised me: a dead connection often doesn't fire a close event. The phone goes through a tunnel, the socket is gone, but the client sits there believing it's connected because nothing told it otherwise. It'll happily accept sends into the void.
So you can't trust the socket's own state. You need a heartbeat, and crucially it has to be two-way — the client pings, the server pongs, and if the pong doesn't arrive within a window, the client declares the connection dead itself and reconnects. Trusting readyState is how you get drivers whose location silently stopped updating twenty minutes ago.
let missed = 0;
setInterval(() => {
if (missed >= 2) return hardReconnect(); // we decide it's dead
missed += 1;
socket.send(JSON.stringify({ t: 'ping' }));
}, 15000);
onMessage((m) => { if (m.t === 'pong') missed = 0; });Reconnect storms are self-inflicted outages
Second lesson, learned the embarrassing way. If your server restarts, every client notices at the same instant and reconnects at the same instant. A fleet's worth of devices hitting you simultaneously is a denial of service you built yourself.
Exponential backoff with jitter, and a cap. The jitter matters more than the backoff — it's what spreads the herd out. Without it you've just synchronised everyone onto the same slower schedule.
const delay = Math.min(30000, 500 * 2 ** attempt);
const jittered = delay * (0.5 + Math.random() * 0.5);
setTimeout(connect, jittered);Reconnecting isn't resuming
This is the one that actually bit us. You reconnect, the socket is open, everything looks healthy — and the client is quietly missing every message sent during the gap. The UI looks fine. It's just wrong.
Every client needs to track the last message it successfully processed and ask for everything since, on reconnect. Which means the server needs a short replay buffer, which means messages need sequence numbers, which means this is a real feature and not a five-line fix.
- Client stores the last sequence number it handled
- On reconnect it sends that number as part of the handshake
- Server replays anything newer from a bounded buffer
- If the gap is bigger than the buffer, the client does a full refetch instead of pretending
That last branch is the one people skip, and it's the one that matters. Sometimes the honest answer is 'I've been offline too long, throw away my state and start again'.
Real-time isn't about how fast a message arrives. It's about what your app believes during the seconds it isn't arriving.
Design the offline state, don't inherit it
The last thing, and it's a design decision more than a technical one: decide what the app looks like when it's disconnected, and build that on purpose.
For the driver app, that meant queueing actions locally and showing an explicit 'not synced' state rather than optimistically pretending everything went through. A driver who thinks they logged a defect and didn't is a worse outcome than a driver who can see it's pending.
Authentication expires mid-connection
One more thing that only shows up in production. You authenticate the socket at connect time, and then the connection stays open for eight hours. Somewhere in hour three the token expires.
A surprising number of implementations just… keep going, because the check only ran once at the handshake. That's a session that outlives its own authorisation, which is a problem if someone's access was revoked in the meantime.
We re-validate periodically over the existing connection and let the client refresh its token in place. If the refresh fails the server closes the socket with a specific code, and the client knows to send the user back to login rather than entering its reconnect loop — otherwise a revoked user reconnects forever against a server that keeps refusing them.
socket.onclose = (e) => {
if (e.code === 4401) return redirectToLogin(); // auth is gone, don't retry
scheduleReconnect(); // anything else, back off and retry
};Optimistic UI is lovely when the network is fine. When the network is the actual problem, honesty beats optimism every time.