All resources
Event Systems10 min read

WebSocket reconnection and presence without false certainty

Build reconnecting clients, heartbeats, backoff, and presence state that remain honest across mobile networks and process restarts.

A practical PingFlow guide for developers working at the boundary between systems.

At a glance

Key takeaways

  • A connection is not a user state
  • Design reconnect with backoff and jitter
  • Use heartbeats and bounded liveness
In this guide

A connection is not a user state

A WebSocket connection tells you that one process currently has a transport open. It does not prove that a user is watching the screen, that the device has a stable network, or that the server will receive the next message. Mobile networks suspend applications, proxies close idle connections, and a process can disappear without a clean close frame. Model presence as a time-bounded observation rather than a permanent boolean.

Define what presence means for the product. It might mean a client sent a heartbeat recently, a document was opened, or a user explicitly marked themselves available. Store the observation time, connection or session ID, and device-safe context. Consumers should tolerate a short delay and a transition to unknown. A green dot that remains for hours after a laptop sleeps is worse than a conservative away state.

Design reconnect with backoff and jitter

A client should reconnect after an unexpected close, but not all clients should reconnect at the same instant after an outage. Use exponential backoff with jitter, a maximum delay, and a cap on attempts or elapsed time. Reset the backoff only after a stable connection has remained open for a meaningful interval. A page that reconnects immediately on every error can create a retry storm that keeps the service unhealthy.

Respect visibility and network state when the platform exposes them. A backgrounded mobile tab may not need an active connection. Pause or reduce reconnect attempts when the user cannot benefit, then perform a fresh authentication and subscription handshake when the tab returns. Do not keep a connection alive solely to preserve an optimistic UI status.

Use heartbeats and bounded liveness

TCP can remain apparently connected after a path has failed, so the application needs a heartbeat or protocol ping. The server should close a connection that misses a bounded number of responses, and the client should treat a missed heartbeat as unknown rather than as a confirmed online state. Set the interval from proxy idle timeouts and device power constraints. A heartbeat that runs every second can drain batteries and still miss a process suspension.

Record last-seen time on the server and expire presence after a grace period. A clean disconnect can mark a session offline immediately, but an unclean disconnect should wait for the lease to expire. If multiple devices are allowed, aggregate their observations by account and device without exposing connection IDs to other users.

Resume events without assuming no gaps

A client can disconnect after the server publishes an event and before the browser receives it. On reconnect, use a durable sequence or cursor to request missed events when the product requires it. The server should bound replay size and expire old events. If a gap cannot be repaired, send a state snapshot or tell the client to refresh rather than pretending the local view is complete.

Make event application idempotent. A reconnecting client may receive a duplicate around the replay boundary. Include an event ID or sequence, apply updates in order where required, and ignore a duplicate that is already durable. Presence updates are often coalescible; financial or audit events are not. Use separate channels or policies for each class.

Authenticate every connection and subscription

A WebSocket handshake should authenticate the session and authorize each subscription. Do not rely on a room or document ID supplied by the client. Revalidate permissions when a long-lived connection remains open, especially after membership or entitlement changes. Use short-lived connection tokens or a session that can be revoked, and never put a long-lived secret in a URL where proxies and logs can record it.

Limit message size, subscription count, heartbeat cost, and idle lifetime. Validate every message as untrusted input. A connection that can subscribe to arbitrary channels is an authorization bug even if the client hides the button. Log connection and subscription decisions with safe identifiers and keep payload content out of routine diagnostics.

Observe connection health at scale

Track active connections, connection age, handshake failures, close codes, reconnect attempts, heartbeat timeouts, subscription denials, replay gaps, and message latency. Break down metrics by region, client version, and network-safe category. A rise in reconnects may indicate a proxy timeout, a bad deploy, a provider edge issue, or a client update that closes sockets aggressively.

Use traces or correlation IDs for a small sample rather than logging every message. A dashboard should show whether users are receiving current state, not only how many sockets are open. Presence correctness depends on expiration and aggregation, so inspect the age of the oldest active observation and the number of unknown sessions.

Test network reality

Test clean close, abrupt process loss, proxy idle timeout, offline and online transitions, tab suspension, duplicate events, replay gaps, token expiry, permission revocation, and a server restart. Simulate a large reconnect storm and verify backoff, admission control, and message replay. Check that a stale presence indicator expires even when no clean close is received.

A reliable realtime interface embraces disconnection. Use bounded liveness, jittered reconnection, durable event IDs, authorization at subscription time, and an honest unknown state. The connection is a transport; the product promise belongs in the event and presence model built around it.

Give users an honest connection state

Expose states such as connected, reconnecting, offline, and unknown instead of turning every open socket into a confident online badge. Let the UI explain when data may be stale and provide a refresh path for a replay gap. A brief unknown state is preferable to showing a user as active after the network has disappeared.

At the service boundary, keep presence updates separate from durable business events. Coalesce rapid heartbeat changes, expire observations, and preserve sequence numbers for events that must be replayed. This distinction reduces storage and fan-out work while preventing a presence optimization from dropping an audit record.

Design for missed events

On reconnect, send a last-seen sequence or cursor and let the server replay the bounded gap. If the gap is too old, return a snapshot plus a new cursor instead of pretending the client is current. Test duplicate messages, out-of-order delivery, tab suspension, mobile network changes, and a server restart. Presence can be approximate, but durable events need an explicit recovery path so a temporary disconnect does not become silent data loss.

Implementation example

Give every durable event a sequence or cursor and keep presence separate from business history. On reconnect, send the last-seen cursor; replay a bounded gap or return a fresh snapshot with a new cursor. Expose connected, reconnecting, offline, and unknown states so the UI never treats an open socket as proof of current presence.

json
{"type":"events.replay","from":1842,"to":1860,"events":[...],"next_cursor":"1860"}

Verify and troubleshoot

Test duplicate, out-of-order, missed, replay-too-old, tab-suspension, mobile-network, server-restart, and heartbeat-expiry paths. Assert that durable events are not lost or applied twice and that a stale presence badge becomes unknown. Measure reconnect time, replay gap, snapshot rate, fan-out, and sequence errors.

Operations and recovery

Bound event history and heartbeat storage, coalesce rapid presence updates, and alert on replay failures or growing gap age. Keep a snapshot rebuild path when a client is too far behind. If a socket fleet or broker is unhealthy, degrade presence honestly while preserving durable events for later replay.

References and further reading

Use the WebSocket protocol documentation, the selected broker's delivery and replay semantics, and browser lifecycle guidance. Define which messages are durable, ephemeral, ordered, or safe to drop.

Keep exploring