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:
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());import BirdRealtime
let bird = BirdRealtime(options: .init(appKey: "your-app-key", region: "us1"))
bird.onConnectionStateChange { previous, current in
print(previous, "->", current)
if current == .unavailable { showReconnectingBanner() }
if current == .connected { hideReconnectingBanner() }
}import com.bird.realtime.BirdRealtime
import com.bird.realtime.BirdRealtimeOptions
import com.bird.realtime.ConnectionState
val bird = BirdRealtime(BirdRealtimeOptions(appKey = "your-app-key", region = "us1"))
bird.onConnectionStateChange { previous, current ->
println("$previous -> $current")
if (current == ConnectionState.UNAVAILABLE) showReconnectingBanner()
if (current == ConnectionState.CONNECTED) hideReconnectingBanner()
}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.
bird.connection.bind("signin_error", ({ message }) => {
console.warn("connection has no identity:", message);
});bird.onSigninError { error in
print("connection has no identity:", error.message)
}bird.onSigninError { error ->
println("connection has no identity: ${error.message}")
}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.
const room = bird.subscribe("presence-room-1");
room.bind("bird:subscription_succeeded", ({ members }) => {
renderMembers(members);
});guard let room = bird.subscribe("presence-room-1") as? PresenceChannel else { return }
room.bind(BirdProtocol.Event.subscriptionSucceeded) { _ in
renderMembers(room.members)
}val room = bird.subscribe("presence-room-1")
if (room is PresenceChannel) {
room.bind(BirdProtocol.Event.SUBSCRIPTION_SUCCEEDED) {
renderMembers(room.members)
}
}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.
bird.disconnect(); // state: disconnected, no reconnect
bird.connect(); // reopens and re-subscribesbird.disconnect() // state: disconnected, no reconnect
bird.connect() // reopens and re-subscribesbird.disconnect() // state: DISCONNECTED, no reconnect
bird.connect() // reopens and re-subscribesOn 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:
room.bind("bird:subscription_error", (err) => {
console.warn("could not join:", err);
});room.bind(BirdProtocol.Event.subscriptionError) { error in
print("could not join:", error ?? "")
}room.bind(BirdProtocol.Event.SUBSCRIPTION_ERROR) { error ->
println("could not join: $error")
}Everything the wire does not attribute to a channel arrives on the connection instead:
bird.connection.bind("error", ({ code, message }) => {
console.warn(code, message);
});bird.onError { error in
print(error.code ?? 0, error.message)
}bird.onError { error ->
println("${error.code} ${error.message}")
}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.
- A connection that never authorized. The app requires authorized connections and this one did not authorize in time. See Requiring authorized connections.
The client does not reconnect automatically, so your app decides what happens next:
bird.connection.bind("error", ({ code }) => {
if (code === 4009) showSignedOutScreen();
});bird.onError { error in
if error.code == 4009 { showSignedOutScreen() }
}bird.onError { error ->
if (error.code == 4009) showSignedOutScreen()
}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 is the contract your backend implements, and the request the client repeats on every reconnect.
- Requiring authorized connections turns an unauthorized connection into a closed one.
- Terminating member connections is how a connection gets closed with 4009 on purpose.
- Realtime overview covers channels, members, and connections as a model.