Reconnection and Recovery
This page explains how Celeris reconnects, what it restores, when it stops, and what your application must recover.
A reconnection opens a new socket after an established connection fails. Recovery is the work that follows. The SDK restores subscriptions, and your application reconciles the messages that it missed during the outage.
The SDK handles the connection. Your application still owns its data. A connected socket is not proof that every missed message arrived.
Read Channels and Segments before this page. Read Connection and Lifecycle for the JavaScript event handlers that you can use in your application.
1. Know when recovery starts
Automatic recovery starts only after a channel reaches connected. A socket close or transport error starts it.
| Event | What happens |
|---|---|
| An established socket closes or encounters a transport failure | Enter reconnecting and schedule an attempt |
The first connect() fails | Reject that call and enter failed. Your application decides whether to call connect() again |
| A server permission denial or rate limit | Report the server error. Keep the connection open |
| A frame cannot be decoded | Drop that frame, report the error, and keep the connection open |
| Your message listener throws | Contain and report the listener error. Keep the connection open |
Your application calls close() | Cancel recovery and close permanently |
failed means that automatic attempts stopped. It does not mean that the channel is permanently unusable. closed is permanent. Create a new channel handle to connect again.
2. Wait between attempts
The SDK uses exponential backoff. The maximum wait increases after failed attempts. With full jitter, the SDK chooses a random delay between zero and that maximum. This spreads the clients out, so they do not all retry together.
| Failed reconnect attempts already used | Next delay range |
|---|---|
| 0 | 0 to 0.5 seconds |
| 1 | 0 to 1 second |
| 2 | 0 to 2 seconds |
| 3 | 0 to 4 seconds |
| 4 | 0 to 8 seconds |
| 5 | 0 to 16 seconds |
| 6 or more | 0 to 30 seconds |
The formula is random() * min(30 seconds, 500 milliseconds * 2^index). These are delay ceilings, not fixed waits.
The retry budget is 10 failed reconnect attempts by default. To change it, set the maximum reconnect attempts option to an integer from 1 to 100. The option is maximumReconnectAttempts in JavaScript and Java, MaximumReconnectAttempts in Go, and maximum_reconnect_attempts in Python. The counter increases when a reconnect attempt fails with Transport or Timeout. A successful reconnect does not consume another failure. It also does not reset the failures already used.
At the next disconnect, the budget resets only if the previous connection stayed open for at least 60 seconds. Brief successful connections retain the earlier failure count. An explicit connect() from failed also starts a fresh budget.
Do not use these timings as an application timeout guarantee. Each attempt also takes time. The browser can delay timers. Repeated successful connections change the overall timeline.
3. Fetch fresh credentials and open a socket
For every attempt, the SDK calls your credential provider again. During recovery, the request includes:
| Field | Meaning |
|---|---|
channelReference | The channel to open |
reason | "reconnect" |
disconnectedAt | Number: when this outage began, in Unix milliseconds |
replayLookbackMs | Number: suggested recent-history window in milliseconds |
signal | Cancellation signal to pass to your credential request |
The suggested lookback is the outage duration, rounded up to milliseconds, plus a five-second overlap. The SDK caps it at 4,294,967,295 milliseconds. Failed attempts do not move the start of that outage, so later attempts request a larger window.
A suggestion is not permission. Your application's credential endpoint, or trusted-server claims callback, must authorize replay and explicitly sign replay: { lookbackMs }. It can cap or refuse the requested window. The SDK cannot grant history access by itself.
One configurable deadline covers credential acquisition and the WebSocket handshake together. This is the connect timeout, which is 15 seconds by default. A separate reconnect timeout bounds reconnect attempts, and its default is the connect timeout. Forward the request's signal to fetch. A channel close then also cancels an active credential request. Late results from an abandoned attempt cannot establish the current connection.
See Authentication for the server-side replay policy.
4. Restore subscriptions
When the new socket opens, the SDK restores the interests that your application still holds:
- The SDK restores message subscriptions in registration order, except for
default, which Celeris joins automatically. - The SDK restores presence subscriptions in registration order, together with any presence subscription to
default. Automatic message membership does not subscribe to presence. - The SDK sends the queued publishes after the subscriptions, in the sequence of the calls.
- The SDK reports the state as
connected. - The SDK emits a recovery event with
retryIndex,possibleGaps: true, andpossibleDuplicates: true.
Here, retryIndex is the zero-based failure count used for that successful attempt, not a lifetime count of reconnections.
Your listeners stay registered. Do not add a second copy after every reconnect. To restore interests, the SDK sends the subscription commands. It does not receive permission receipts, and it does not wait for the end of the replay. A server can still reject a subscription through the error handler.
The SDK does not send a publish again if it gave that publish to the socket before the connection closed. Only rate-limit recovery sends a publish again.
5. Handle calls made during an outage
| Your operation | While reconnecting |
|---|---|
| Publish | Waits in the queue. The SDK sends it after the reconnect |
| Query presence | Rejects with NotConnected |
| Subscribe to messages or presence | Records local interest for restoration |
| Cancel a subscription | Removes local interest, so the SDK does not restore it |
Call connect() again | Rejects with OperationInProgress |
Call close() | Cancels the retry timer and any active attempt, then closes |
A publish that the SDK did not give to the socket before the connection closed also waits in the queue. The queue limit still applies. When the queue is full, a publish rejects with Backpressure. When recovery stops in failed, the queued publishes reject with the error that stopped recovery. When you close the channel, they reject with Cancelled. A publish before the first connect, during the first connect, or after failed rejects with NotConnected.
A presence query already in flight when the connection drops rejects with Transport. The SDK does not retry that query. Your application can issue a new one after recovery.
A practical interface disables the send controls during a disconnection. It shows the current connection state. Do not automatically retry a publish with uncertain delivery. It is possible that the recipient already received the message.
6. Understand when automatic attempts stop
During reconnection, Transport and Timeout failures consume the retry budget and schedule another attempt. This continues until the number of failures in the current budget is equal to the maximum reconnect attempts, which is 10 by default.
The SDK closes a socket that refuses a subscription-restoration write. This counts as a Transport failure. Other attempt failures stop recovery immediately. Examples are malformed credential-provider output (Configuration) and cancellation. An explicit close instead ends in closed.
When recovery fails, the SDK invalidates old attempts and timers. It reports the terminal error through onError and enters failed. The queued publishes reject with the same error. Your application can offer a Retry button that calls connect(). That explicit call resets the retry count, the outage information, and the duplicate window. Its credential request has reason: "initial" and no suggested recovery lookback.
A rejected WebSocket handshake appears as Transport. This also applies when credentials caused the rejection. The SDK cannot inspect the handshake's HTTP status, so it cannot classify that as an authentication failure. During recovery, the SDK retries. On the first connection attempt, it rejects the caller.
7. Reconcile application data
Replay is recent, bounded history on the server. It is not a durable log, a public sequence cursor, or a guarantee that every message from the outage is available. The server applies it as follows:
- The connection gets the replay of
defaultwhen it connects. It gets the replay of each other segment when it joins that segment withsubscribe(). A publish starts no replay. - The server keeps at most the last 100 messages of each segment, for at most 2 minutes. Replay sends them in publish order, with their original message IDs.
- A numeric
replayclaim sends only the messages from that window. A token without a replay claim gets no history. - After a reconnect, the SDK subscribes again to the held subscriptions, and each join replays the messages that the outage missed. When the reconnect credentials have no replay claim, those messages are lost, and the recovery event declares the gap.
The client remembers 1,024 recently delivered message IDs per channel by default (the deduplicationWindowSize option). That window survives automatic reconnects. It filters repeated IDs before the SDK delivers the messages to listeners. An older duplicate can still arrive after its ID leaves the window. An explicit connect() clears the window.
After the recovery event:
- Fetch important state from your application's authoritative API.
- Reconcile live messages with that snapshot through application-owned versions or identifiers.
- Refresh presence when a current connection list matters.
- Keep irreversible actions idempotent in your application.
For a chat application, reload stored messages from your database. For an order dashboard, fetch the latest order status. Celeris transports messages. It does not replace that stored state.
There is no durable offline queue, delivery receipt, replay-complete marker, or global ordering guarantee. The publish queue is in memory only. A reconnect does not send a publish again if the SDK gave that publish to the socket before the connection closed. Read Message Ordering for the ordering boundaries. Then read Connection and Lifecycle to handle recovery in JavaScript.