# Connection

A chat connection can fail in two ways. The device can lose its network, or the client's WebSocket can drop while the network is fine: the server closed it, the token expired, or a health check timed out. The client reports the two separately, so an app can tell "you're offline" from "reconnecting".

When the socket comes back, the client brings what the user is looking at up to date by itself, then announces it with `connection.recovered`.

On this page, a **reconnect** is the client's WebSocket opening again after it dropped: when the network comes back, when the device wakes from sleep, when the client reopens a socket the server closed, or after `client.closeConnection()` and `client.openConnection()`. The client's first connection isn't a reconnect.

This page covers both: the connection status, then recovery. Showing a lost-connection banner is on [Connection status](https://getstream.io/chat/docs/sdk/react/features/app-experience/connection-status/), and reading these stores in components on [Reading state in components](https://getstream.io/chat/docs/sdk/react/ui/reading-state-in-components/).

## Connection status

| Question                                  | Where to read it                             |
| ----------------------------------------- | -------------------------------------------- |
| Is the WebSocket up?                      | `client.wsConnection.state`, `isHealthy`     |
| Does the device have a network?           | `client.networkConnection.state`, `isOnline` |
| Has the client caught up after reconnect? | the `connection.recovered` event             |

The two stores can disagree either way. A socket can drop on a working network. And when the device loses its network, the socket can look healthy for up to 35 seconds, until its next health check fails, unless something tells the client about the loss. In a browser, the client learns it at once from the browser's `offline` event and marks the socket down. Elsewhere you install a network status reporter, as [Device network](#device-network) explains.

No event announces when the socket goes up or down, or when the device goes online or offline. To follow them, subscribe to `client.wsConnection.state` for the socket, or `client.networkConnection.state` for the network.

### Socket health

`client.wsConnection.state` holds `{ isHealthy, lastHealthyAt, lastUnhealthyAt }`. `isHealthy` is always a boolean, so `!isHealthy` is safe. `client.wsConnection.isHealthy` reads the current value.

The store records every change, including a deliberate `client.closeConnection()`, and it records a drop the moment it happens. Most drops recover in under a second, so a UI that shows every drop at once makes a working app look broken. Wait `offlineNotificationDisplayDelayMs` before showing one, and cancel the wait if the socket comes back:

```ts
const delayMs = client.wsConnection.config.offlineNotificationDisplayDelayMs;
let timer: ReturnType<typeof setTimeout> | undefined;

const unsubscribe = client.wsConnection.state.subscribeWithSelector(
  ({ isHealthy }) => ({ isHealthy }),
  ({ isHealthy }) => {
    clearTimeout(timer);
    if (isHealthy) {
      console.log("Connected");
      return;
    }
    timer = setTimeout(() => console.log("Reconnecting…"), delayMs);
  },
);
```

The socket's timings are set through `client.config`, which [Configuration](https://getstream.io/chat/docs/sdk/react/state/state-and-entities/configuration/) explains:

```ts
client.config.set({
  client: {
    wsConnection: {
      connectTimeoutMs: 15_000, // how long connecting waits for the server
      healthCheckGracePeriodMs: 10_000, // extra time before a silent socket counts as dead
      offlineNotificationDisplayDelayMs: 5_000, // How long a UI should wait before showing a drop
      pingIntervalMs: 25_000, // how often a health check goes out
    },
  },
});
```

The values above are the defaults. `client.wsConnection` takes these options:

| Option                              | Default                | What it does                                                                                                                                                                                                                                                       |
| ----------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `connectTimeoutMs`                  | 15 s                   | How long connecting waits for the server before it fails.                                                                                                                                                                                                          |
| `pingIntervalMs`                    | 25 s                   | How often a health check goes out. It can only be lowered, to 1 s at least: a slower ping risks the server closing the connection as idle. Lowering it notices a dead socket sooner.                                                                               |
| `healthCheckGracePeriodMs`          | 10 s                   | Extra time before a silent socket counts as dead: after `pingIntervalMs + healthCheckGracePeriodMs` without an answer, 35 s by default. At least 1 s.                                                                                                              |
| `offlineNotificationDisplayDelayMs` | 5 s                    | How long a UI should wait before showing a drop. The client doesn't act on it. It's there so every UI waits the same time.                                                                                                                                         |
| `enableWSFallback`                  | `false`                | `true` falls back to HTTP long-polling when the WebSocket can't connect, for networks that block WebSockets. The socket store then follows the long-poll.                                                                                                          |
| `webSocketImpl`                     | the global `WebSocket` | The `WebSocket` constructor the client opens the socket with.                                                                                                                                                                                                      |
| `urlParams`                         | none                   | Extra query parameters added to the WebSocket URL.                                                                                                                                                                                                                 |
| `connection`                        | none                   | A `StableWSConnection` built in advance, used instead of opening a socket. It's meant for tests: the client uses it only outside a browser, when there's no global `window` or the client's `browser` setting is `false`, and `client.config.reset()` discards it. |

A timing value outside its bounds is moved to the nearest bound, and the client logs a warning.

### Device network

`client.networkConnection.state` holds `{ isOnline, lastOnlineAt, lastOfflineAt }`, and `client.networkConnection.isOnline` reads the current value.

`isOnline` is `undefined` when the status is unknown: nothing has reported it yet. Test it with `=== false`:

```ts
if (client.networkConnection.isOnline === false) {
  // The device is offline.
}

// Wrong: also true while the status is unknown, such as before the first connection.
if (!client.networkConnection.isOnline) {
  // ...
}
```

The client can't detect the network by itself: every platform reports it differently. So a **reporter** tells it. A reporter is a function that reports the current status, reports again on every change, and returns a function that stops it.

In a browser, the client installs a reporter for you, based on `navigator.onLine` and the `online` and `offline` events. Install your own when you know the network status better than the browser does, for example from a host app that embeds the chat in a web view. Outside a browser, until a reporter is installed, the network status is inferred from the socket's connection state, which can't tell a dead socket from a lost network.

```ts
import type { NetworkStatusReporter } from "stream-chat";

const reporter: NetworkStatusReporter = (onStatusChange) => {
  // Report the current status right away, then on every change.
  onStatusChange(navigator.onLine);

  const handle = () => onStatusChange(navigator.onLine);
  window.addEventListener("online", handle);
  window.addEventListener("offline", handle);

  return () => {
    window.removeEventListener("online", handle);
    window.removeEventListener("offline", handle);
  };
};

client.config.set({
  client: { networkConnection: { statusReporter: reporter } },
});
// or, with the same effect:
client.networkConnection.setStatusReporter(reporter);
```

`client.networkConnection` takes this option:

| Option           | Default                                                                                                         | What it does                                           |
| ---------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `statusReporter` | in a browser, one based on `navigator.onLine`; elsewhere, one that infers it from the socket's connection state | The function that reports the device's network status. |

The client uses the reporter installed last: a `setStatusReporter()` call replaces a reporter from `client.config`, and setting `statusReporter` in `client.config` replaces one from `setStatusReporter()`. A `client.config.set()` call that doesn't set `statusReporter` keeps the installed reporter. Installing the same function again does nothing, and `client.networkConnection.setStatusReporter(null)` removes the reporter and keeps the last status.

If the reporter throws while the client installs it, the client logs an error and installs nothing in its place, so `isOnline` stops changing. A host with no event to listen to can report the status directly with `client.networkConnection.setStatus(isOnline)`.

Don't wait for `isOnline` to be `true` before sending requests. Requests don't depend on it, and they work while it's `undefined`. A reporter only makes the client faster: it notices a lost network before the socket's health check fails, and reconnects as soon as the network comes back instead of waiting for its next retry.

## Connection recovery

### What reloads after a reconnect

>
> **Info:** **Example**
>
> The user's laptop sleeps for ten minutes. While it sleeps, they miss messages, reactions and read receipts, and the server ends every watch their client held. When the socket comes back, the client:
>
> 1. **Refreshes every channel paginator that has loaded.** Each paginator queries again, page by page, the channels it has loaded, up to 150, and the queries watch those channels again. The paginator keeps its channels meanwhile. Then it removes the loaded channels no page returned: channels that were deleted, that the user left, or that were hidden, archived or stopped matching the paginator's filters while the client was disconnected. Channels loaded beyond the first 150 leave the paginator, and load again when the paginator loads more. The client still holds them, with `watchStatus` in `channel.state` set to `wasWatching`, as explained below.
> 2. **Reloads every active channel.** An active channel is one something opened with `Channel.activate()`. The channel watches again and reloads its newest loaded interval of messages. Messages in that interval that were deleted while the client was disconnected disappear, and the user's unsent messages stay. Older intervals that a jump loaded aren't reloaded.
> 3. **Reloads every active thread's replies.**
> 4. **Marks every other thread the client holds as stale.** A stale thread loads again the next time it's opened.
> 5. **Dispatches `connection.recovered`** once the active channels and threads have reloaded. The channel paginators refresh alongside them, and can finish later.
>

After that event, the thread list loads again, if it was opened in this session.

A first connection isn't a reconnect, so none of this runs then. `<Channel/>` activates the channel it shows, so that channel reloads. `<Thread/>` doesn't activate its thread. A thread reloads only while something holds it active, such as `useActiveThread`, which the app calls for the thread it shows. Otherwise it's marked stale and loads again when it's opened. [Entity lifecycle](https://getstream.io/chat/docs/sdk/react/state/state-and-entities/entity-lifecycle/) explains `Channel.activate()` and `Thread.activate()`.

### Channels re-watched on demand

A watch is what makes the server send a channel's events to this client. The server ties watches to the connection, so a reconnect ends them all. The recovery steps watch again the channels each paginator loaded again, and the active channels.

The other channels the client holds aren't watched again. Their `watchStatus` in `channel.state` stays `wasWatching`. These include channels beyond the first 150 a paginator had loaded, channels opened earlier that aren't active, and channels no paginator holds. Watching them all again could exceed the server's watch limit. Instead, each one is watched again when the channel manager routes it after an event, for example `notification.message_new` when someone writes in it.

### Event `connection.recovered`

`connection.recovered` fires after every reconnect: an automatic one, and `client.closeConnection()` followed by `client.openConnection()`. It doesn't fire on the first connection.

While recovery is on, the client has reloaded its active channels and threads by the time it fires, and its channel paginators are refreshing. Listen for it to refresh what the client doesn't own: results of one-off queries, counters, your own data.

For example, an app that shows the user's total unread count from a one-off query loads it again:

```ts
let totalUnread = 0;

const loadTotalUnread = async () => {
  const { total_unread_count } = await client.unreadCounts();
  totalUnread = total_unread_count;
};

const { unsubscribe } = client.on("connection.recovered", () => {
  void loadTotalUnread();
});
```

If the socket drops again while the client reloads, the event waits for the next reconnect, whose recovery fires it.

### Turning recovery off or running it by hand

Turn recovery off when your app refreshes its data itself. The setting is read at each reconnect, so a change applies from the next one:

```ts
client.config.set({ client: { connectionRecovery: { enabled: false } } });
```

With recovery off, a reconnect refreshes nothing, and nothing is watched again:

| Part                                        | After a reconnect, with recovery off                                                                                                                                                                                                      |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connection.recovered`                      | Fires at once, before anything is reloaded.                                                                                                                                                                                               |
| Channel lists (channel paginators)          | Not refreshed. They keep the channels and the order they had before the drop, and their channels aren't watched again.                                                                                                                    |
| `Channel` instances, including the open one | Not reloaded and not watched again, so they get no events. A channel is watched again only when an event for it arrives, such as `notification.message_new`, as [Channels re-watched on demand](#channels-re-watched-on-demand) explains. |
| `Thread` instances, including the open one  | Not reloaded and not marked stale. A thread needs its own reload: watching its channel again doesn't reload its replies.                                                                                                                  |
| Thread list                                 | Reloads, if it was opened in this session. The threads in it keep their old replies and counts.                                                                                                                                           |

So refresh what the user sees in a `connection.recovered` listener:

| To refresh                                                                               | Call                                                                                                                                                                                                         |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| the channel lists                                                                        | `client.channelManager.recover()`. It refreshes each channel paginator that has loaded, as step 1 of [What reloads after a reconnect](#what-reloads-after-a-reconnect) does, and watches its channels again. |
| the open channel                                                                         | `Channel.reload()`. It watches the channel again and reloads its newest messages.                                                                                                                            |
| the open thread, the threads shown in the thread list, and a thread the user opens again | `Thread.reload()`.                                                                                                                                                                                           |

`Channel.reload()` and `Thread.reload()` reject when the request fails, so catch the error in your listener.

To run the SDK's recovery yourself, with recovery on or off, call `client.connectionRecovery.recover()`. It does the steps in [What reloads after a reconnect](#what-reloads-after-a-reconnect):

- It fires `connection.recovered` once the active channels and threads have reloaded. The channel paginators can finish later.
- It holds the event back if the socket dropped while it ran, or is down when it finishes.
- It does nothing while the active channels and threads are still reloading.
- Don't call it from a `connection.recovered` listener. It fires the event itself, so the listener would call it again and again.

## Used in

- [Connection status](https://getstream.io/chat/docs/sdk/react/features/app-experience/connection-status/)
- [Notifications UI](https://getstream.io/chat/docs/sdk/react/ui/components/notifications-ui/)
- [System notification banner](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/notifications/system-notification-banner/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/state/connection/](https://getstream.io/chat/docs/sdk/react/state/connection/).