Software Engineering
WebSocket Reconnects: Resume Without Losing Events
Design reconnecting WebSocket clients with durable cursors, duplicate handling, replay limits, explicit gap recovery, and tests for interrupted connections.
In this article
A reconnected WebSocket is a new connection, not proof that the client received every event from the previous one. If an application needs continuity, it must define how to resume, replay, deduplicate, and recover from gaps. Simply reopening the socket can leave a dashboard looking healthy while silently missing updates.
RFC 6455 specifies the WebSocket protocol. Durable event history and application-level resume cursors are separate design choices. This guide describes one practical approach for applications you control, not a feature guaranteed by every WebSocket server.
Define the event contract
Give each event a stable identifier within a clearly defined stream. Decide whether ordering applies globally, per account, per document, or per partition. A timestamp alone may be insufficient because different events can share a time and clocks can disagree.
Include a revision or cursor that the client can use when requesting later events. Do not assume that an opaque cursor is a numeric counter unless the server explicitly guarantees it. Treat the cursor as server-defined data and bind it to the authorized stream.
Specify what “processed” means on the client. Receiving bytes is not the same as successfully applying an event to application state. If the client advances its cursor before applying the update and then crashes, it can skip the event on the next connection.
Persist progress at the right point
Apply an event and record the resulting progress together where the client architecture permits. For a durable local store, consider a transaction that updates both state and cursor. For an in-memory dashboard, document that a full snapshot is the recovery source after reload.
Avoid keeping the only progress marker inside the socket object. That object disappears when the connection ends. Also avoid storing sensitive event content unnecessarily; a minimal stream identifier and cursor may be sufficient for the resume protocol.
If the application cannot atomically update state and progress, make repeated events safe to apply. For example, applying a record's revision 42 twice should not charge a customer twice or increment a counter twice. Financial or other consequential actions need stronger server-side controls than a browser's memory.
Make reconnection bounded
After a disconnect, reconnect with a delay that increases and includes jitter. Keep an upper bound and distinguish authentication failures from temporary transport failures. An expired or revoked session should not trigger endless reconnect attempts with the same credentials.
On a successful connection, authenticate the stream and send the last processed cursor through the supported protocol. The server should authorize access before replaying events. A client-supplied account identifier is not proof that the client may read that account's history.
Reset the retry behavior deliberately after stable service, not after a connection that immediately fails again. Otherwise a failing service can produce a fast loop that looks like repeated successful recovery. Our backoff and jitter guide explains the retry-load issue in more detail.
Design replay and gap detection
The server needs a retained event history or another way to reconstruct the state. A replay buffer has a finite retention period and capacity. If the client's cursor is older than that history, return an explicit gap or resynchronization response rather than pretending that no events occurred.
For example, a client last processed event 100, but the retained history begins at 150. The server cannot honestly resume every intervening event from that buffer. The client should fetch an authorized snapshot and a corresponding new cursor, then continue from that consistent point.
Make the snapshot-to-stream handoff precise. Fetching a snapshot and then starting an unrelated live stream can create another gap between the two actions. Define how the snapshot's revision relates to the stream cursor so updates during the fetch are replayed or otherwise included.
Handle duplicates and ordering
Replays commonly include an event the client already processed, especially if acknowledgments were lost. Treat delivery as potentially repeated unless the system proves a stronger guarantee. Keep deduplication scoped to the stream and retention requirements rather than storing every identifier forever.
Out-of-order events need an explicit policy. If revision 43 arrives before 42, the client might buffer, request recovery, or reject the update depending on the contract. Do not silently apply older state over newer state. A latest-value dashboard and an ordered transaction feed have different needs.
Avoid claiming exactly-once behavior because a reconnect test passed. End-to-end consequences involve the event source, transport, storage, and consumer action. State what your implementation actually guarantees and which failure cases remain possible.
Test interrupted processing
Use synthetic events in a disposable environment. Disconnect before receipt, after receipt but before processing, after processing but before acknowledgment, and after acknowledgment. Compare the resulting state with the expected sequence.
Then test a cursor outside retention, a changed permission, a server restart, and two active clients for the same account. Verify that recovery doesn't expose another tenant's history or overwrite newer state. Include slow consumers so buffering does not grow without limit.
JSON Diff can compare redacted final state from a clean run and an interrupted run. It is a comparison utility, not a replay implementation. Keep the event log and cursor observations in your own approved test environment.
Conclusion
Reliable WebSocket recovery is an application protocol: stable events, processed-progress tracking, bounded retries, authorized replay, and explicit gap recovery. A socket that reconnects is only the beginning. Verify the final state, not just the connection indicator.