# Connection lifecycle and reconnection

A Realtime connection is one WebSocket, and it will not stay open forever. Laptops sleep, phones change networks, load balancers cycle, and your app has to keep working through all of it. The client handles the reconnecting on its own, so most apps only need to know enough to render the right thing while it happens.

## The six states

`bird.connection.state` is always one of these:

| State          | What it means                                                     |
| -------------- | ----------------------------------------------------------------- |
| `initialized`  | The client exists and has not opened a socket yet.                |
| `connecting`   | A socket is opening, or is open and waiting for the handshake.    |
| `connected`    | The handshake landed. The connection has an id and can subscribe. |
| `unavailable`  | The connection dropped and a reconnect is scheduled.              |
| `disconnected` | You called `disconnect()`. Nothing is scheduled.                  |
| `failed`       | The server refused this connection. The client will not retry.    |

Bind `state_change` to see every transition, or bind a single state name when only one matters:

```typescript
import { BirdRealtime } from "@messagebird/realtime";

const bird = new BirdRealtime({ appKey: "your-app-key", region: "us1" });

bird.connection.bind("state_change", ({ previous, current }) => {
  console.log(previous, "->", current);
});

bird.connection.bind("unavailable", () => showReconnectingBanner());
bird.connection.bind("connected", () => hideReconnectingBanner());
```

`connecting` covers more ground than the name suggests. An open socket is not a usable connection: the client stays in `connecting` until the server's handshake frame arrives, which is the moment `bird.connection.connectionId` is set and the state becomes `connected`. That ordering is what makes authorization possible, since every signature your backend computes is over a connection id. A handshake that arrives without one is a hard failure, and the client reports an error and closes rather than sitting in a `connected` state it cannot subscribe from.

## Reconnection is automatic, and jittered

After an unexpected drop the client waits, then opens a new socket. The delay grows exponentially from one second and is capped at thirty, with full jitter applied to each wait, so a fleet of browsers dropped by the same event does not come back in lockstep and hammer the edge on the same tick. Each successful handshake resets the backoff.

The close code decides which of three things happens:

| Close code       | Behavior                                              |
| ---------------- | ----------------------------------------------------- |
| `4000` to `4099` | Refused. No retry, state goes to `failed`.            |
| `4200` to `4299` | Retry immediately.                                    |
| Everything else  | State goes to `unavailable`, then retry with backoff. |

Ordinary network trouble falls in the third row, which is the common case: a dropped Wi-Fi connection, a sleeping tab, a proxy timing the socket out. The first row is the one worth handling in your UI, because a refusal is terminal and the code is the only explanation your app gets.

## The stale-connection check

A socket can look open long after the path behind it is gone, so the client does not wait for the network to admit it. It arms an activity timer (120 seconds by default, and the server can lower it through the handshake's `activity_timeout`) and sends a ping when the timer fires. If no pong comes back within the pong timeout (30 seconds by default), the client force-closes the socket with a code in the retry-immediately band, so a dead connection is replaced in seconds rather than hanging until something else notices.

## What happens to your channels

Subscriptions belong to the connection, so a drop takes all of them with it. The channel objects survive: the client keeps them registered and re-subscribes every one of them on the next `connected`, and you do not rebind your handlers.

Private and presence channels are re-authorized as part of that, with a fresh call to your `authEndpoint` for the new connection id. Old authorization cannot be replayed, because the signature is bound to the connection id it was issued for. The same is true of `signin()`: call it once and the client re-signs in on every later connection, with failures reported on `signin_error` since there is no promise left to reject.

```typescript
bird.connection.bind("signin_error", ({ message }) => {
  console.warn("connection has no identity:", message);
});
```

One consequence catches people out. Between the drop and the re-subscribe, a channel is not subscribed, so `channel.trigger` returns `false` and drops the event. Check `channel.subscribed` before sending a client event, or rebuild from `bird:subscription_succeeded`, which fires again on every reconnect.

```typescript
const room = bird.subscribe("presence-room-1");

room.bind("bird:subscription_succeeded", ({ members }) => {
  renderMembers(members);
});
```

Events published while a client was away are not replayed. Realtime delivers to whoever is subscribed at publish time, so if a view has to be correct after a gap, fetch its current state on `bird:subscription_succeeded` and let the events that follow keep it fresh.

## Disconnecting on purpose

`bird.disconnect()` closes the socket and schedules nothing, which is the right call when the user signs out or navigates away from the part of your app that needs live data. Channels are retained, so a later `bird.connect()` reopens and re-subscribes everything.

```typescript
bird.disconnect(); // state: disconnected, no reconnect
bird.connect(); // reopens and re-subscribes
```

## Errors on the connection versus errors on a channel

Server errors that belong to one channel arrive on that channel. A refused subscription is the usual one, and an authorizer that returns `403` shows up here:

```typescript
room.bind("bird:subscription_error", (err) => {
  console.warn("could not join:", err);
});
```

Everything the wire does not attribute to a channel arrives on the connection instead:

```typescript
bird.connection.bind("error", ({ code, message }) => {
  console.warn(code, message);
});
```

## The close code to actually handle: 4009

`4009` is in the no-retry band, so a connection closed with it goes to `failed` and stays there. Realtime uses it for two different situations, and the reason text is what tells them apart:

- **A terminated member.** Your backend called the disconnect API for this member, so every connection they hold was closed. See [Terminating member connections](/docs/guides/realtime/terminating-member-connections).
- **A connection that never authorized.** The app requires authorized connections and this one did not authorize in time. See [Requiring authorized connections](/docs/guides/realtime/authorized-connections).

Either way the client is done trying, and your app decides what happens next:

```typescript
bird.connection.bind("error", ({ code }) => {
  if (code === 4009) showSignedOutScreen();
});
```

Showing a signed-out state is usually right. Calling `bird.connect()` again works, but the new connection goes through your auth endpoints from scratch, so it only helps if you expect those endpoints to answer differently now.

## Next steps

- [Authorizing channels](/docs/guides/realtime/authorizing-channels) is the contract your backend implements, and the request the client repeats on every reconnect.
- [Requiring authorized connections](/docs/guides/realtime/authorized-connections) turns an unauthorized connection into a closed one.
- [Terminating member connections](/docs/guides/realtime/terminating-member-connections) is how a connection gets closed with `4009` on purpose.
- [Realtime overview](/docs/guides/realtime/overview) covers channels, members, and connections as a model.