---
title: "Connection lifecycle and reconnection"
description: "The six states a Realtime connection moves through, how the client reconnects after a drop, and which close codes it refuses to retry."
canonical: "https://bird.com/docs/guides/realtime/connection-lifecycle"
---

# Connection lifecycle and reconnection

A Realtime connection is one WebSocket. Network changes, sleeping devices, and infrastructure restarts can close it. The client reconnects automatically, while your app can show the current connection state.

## The six states

`bird.connection.state` in the browser client, and `bird.connectionState` in Swift and Kotlin, is always one of these (Kotlin spells them as `ConnectionState` constants, so `unavailable` reads `ConnectionState.UNAVAILABLE`):

| 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 does not retry.    |

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

**TypeScript**

```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());
```

Examples: [TypeScript](/docs/guides/realtime/connection-lifecycle.ts.md) · [Swift](/docs/guides/realtime/connection-lifecycle.swift.md) · [Kotlin](/docs/guides/realtime/connection-lifecycle.kotlin.md)

Swift and Kotlin use one observer for every transition, so branch on `current`. Handlers run on the main queue in Swift and on the Android main looper in Kotlin when available. Pass a `deliveryQueue` or `Executor` to use another execution context.

An open socket remains in `connecting` until the server's handshake arrives. The handshake sets `bird.connection.connectionId` before the state becomes `connected`, so authorization can sign that ID. If the handshake omits the ID, the client reports an error and closes the socket.

## Automatic reconnection

After an unexpected drop, the client waits before opening a new socket. The delay uses full-jitter exponential backoff with a one-second base and a 30-second cap. 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. |

Network failures use the backoff path. Handle refusal codes in your UI because these terminal failures do not reconnect automatically.

## The stale-connection check

A socket can appear open after its network path fails. The client sends a ping after the activity timeout, which defaults to 120 seconds unless the handshake supplies another value. If no pong arrives within the 30-second default pong timeout, the client closes the socket with a retry-immediately code.

## What happens to your channels

Subscriptions belong to the connection, so a drop removes them. The client retains each channel object, re-subscribes after the next `connected` state, and keeps its existing handlers.

The client calls your `authEndpoint` again for each private and presence channel using the new connection ID. It cannot reuse the old signature because that signature includes the previous connection ID. After one `signin()` call, the client also signs in on each new connection and reports later failures through `signin_error`.

**TypeScript**

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

Examples: [TypeScript](/docs/guides/realtime/connection-lifecycle.ts.md) · [Swift](/docs/guides/realtime/connection-lifecycle.swift.md) · [Kotlin](/docs/guides/realtime/connection-lifecycle.kotlin.md)

While a channel is reconnecting, `channel.trigger` returns `false` without sending the client event. Check `channel.subscribed`, or send after `bird:subscription_succeeded`, which fires after every successful re-subscription.

**TypeScript**

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

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

Examples: [TypeScript](/docs/guides/realtime/connection-lifecycle.ts.md) · [Swift](/docs/guides/realtime/connection-lifecycle.swift.md) · [Kotlin](/docs/guides/realtime/connection-lifecycle.kotlin.md)

Realtime does not replay events published while a client is disconnected. If a view must recover after a gap, fetch its current state on `bird:subscription_succeeded` and apply later events from there.

## Disconnecting on purpose

`bird.disconnect()` closes the socket without scheduling a reconnect. Use it when a signed-out or inactive client no longer needs live data. The client retains channels, so a later `bird.connect()` reopens the socket and re-subscribes.

**TypeScript**

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

Examples: [TypeScript](/docs/guides/realtime/connection-lifecycle.ts.md) · [Swift](/docs/guides/realtime/connection-lifecycle.swift.md) · [Kotlin](/docs/guides/realtime/connection-lifecycle.kotlin.md)

On mobile, disconnect when the relevant view or activity stops and reconnect when it resumes. Channel objects and bindings remain available.

## 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**

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

Examples: [TypeScript](/docs/guides/realtime/connection-lifecycle.ts.md) · [Swift](/docs/guides/realtime/connection-lifecycle.swift.md) · [Kotlin](/docs/guides/realtime/connection-lifecycle.kotlin.md)

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

**TypeScript**

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

Examples: [TypeScript](/docs/guides/realtime/connection-lifecycle.ts.md) · [Swift](/docs/guides/realtime/connection-lifecycle.swift.md) · [Kotlin](/docs/guides/realtime/connection-lifecycle.kotlin.md)

## Handle close code `4009`

Code `4009` is in the no-retry band, so the connection enters `failed`. Check the reason to distinguish these situations:

- **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).

The client does not reconnect automatically, so your app decides what happens next:

**TypeScript**

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

Examples: [TypeScript](/docs/guides/realtime/connection-lifecycle.ts.md) · [Swift](/docs/guides/realtime/connection-lifecycle.swift.md) · [Kotlin](/docs/guides/realtime/connection-lifecycle.kotlin.md)

A signed-out state is appropriate when authentication has ended. Calling `bird.connect()` starts a new connection and repeats authorization, so reconnect only after the caller's authorization state changes.

## 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.

## Related resources

- [Realtime](/realtime-api) (product)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first realtime event](/docs/get-started/send-your-first-realtime-event) (docs)

[Get an implementation brief](/learn/workspace?topic=realtime)
