Documentation
Sign inGet started

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:
StateWhat it means
initializedThe client exists and has not opened a socket yet.
connectingA socket is opening, or is open and waiting for the handshake.
connectedThe handshake landed. The connection has an id and can subscribe.
unavailableThe connection dropped and a reconnect is scheduled.
disconnectedYou called disconnect(). Nothing is scheduled.
failedThe 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:
Codebeispiel
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 codeBehavior
4000 to 4099Refused. No retry, state goes to failed.
4200 to 4299Retry immediately.
Everything elseState 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.
Codebeispiel
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.
Codebeispiel
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.
Codebeispiel
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:
Codebeispiel
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:
Codebeispiel
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.
  • A connection that never authorized. The app requires authorized connections and this one did not authorize in time. See Requiring authorized connections.
Either way the client is done trying, and your app decides what happens next:
Codebeispiel
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