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:
Exemplo de código
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.
Exemplo de código
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.
Exemplo de código
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.
Exemplo de código
bird.disconnect(); // state: disconnected, no reconnect
bird.connect(); // reopens and re-subscribesErrors 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:
Exemplo de código
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:
Exemplo de código
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:
Exemplo de código
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 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.