Surviving reconnects: a WebSocket client that never lies
The hard part of consuming a market feed isn't the happy path. It's the laptop lid closing, the phone switching from Wi-Fi to LTE, the corporate proxy that silently drops idle connections. A market-data client that only works on a stable link will, sooner or later, show your users a frozen price and call it live. Here's the discipline we apply in our own clients.
Answer the heartbeat, exactly
CryHub's server pings every connection on an interval. The client must answer with a pong frame echoing the ping's value — and per our protocol, the echoed value must be a string:
ws.send(JSON.stringify({ op: "pong", data: { pong: String(ping) } }));
That type detail has bitten more integrations than anything else in our support inbox. Send it as a number and everything appears fine — until the server, having heard no valid pong, closes the connection some seconds later. The failure shows up displaced from its cause, which is what makes it nasty. Answer immediately, echo exactly, mind the type.
Reconnect like you mean it
Three rules for the reconnect loop:
- Exponential backoff with a cap. Start around one second, double per attempt, cap near thirty. Add jitter — if your product has ten thousand open tabs when a network blip hits, you do not want ten thousand simultaneous reconnects.
- Reset backoff only after the connection proves itself — a welcome frame or a successful subscription ACK, not merely the socket opening. Some middleboxes let connections open and then starve them.
- Never surface a retry state as an error. Users on hotel Wi-Fi don't need a red banner; they need the UI to quietly say "reconnecting" and then to actually recover.
Resubscribe from state, not history
On every (re)connect, the client should rebuild its subscriptions from current application state — the pairs and channels the UI needs now — not replay a log of past subscribe messages. State-driven resubscription makes reconnects idempotent: however chaotic the disconnect, the next session starts from truth. It also collapses a whole class of bugs where a user changed pairs mid-outage and the old pair comes back from the dead.
While the socket is down, one REST snapshot per visible pane bridges the gap so the user never stares at a spinner where a price used to be.
Tell the truth about staleness
Finally, the part most clients skip: track the timestamp of the last tick per subscription. If nothing has arrived for longer than the channel's natural cadence, say so — dim the price, show the reconnect chip, do anything except keep rendering a stale number as if it were current. A feed that admits "stale" is trustworthy. A feed that never admits anything is just occasionally wrong.
None of these patterns is exotic. Together they're the difference between a demo and a terminal.