livqenoDocs

RTCReconnection & Network Quality

Reconnection & Network Quality

What happens when the network drops, and how to show it in your UI — on every SDK.

Networks drop: a user walks out of Wi-Fi range, a train goes into a tunnel, a laptop switches from Wi-Fi to a phone hotspot. When this happens, the SDK reconnects on its own. This page explains what happens, what you can control, and how to show it to users.

All code on this page runs in your app. You don't need any backend code for reconnection.

What happens when the network drops

connectednetwork lostreconnectingbackconnectedevent: reconnected
reconnectingevery retry faileddisconnectederror event: CONNECTION_FAILED
Reconnection, and when it gives up.
  1. The SDK notices the connection is gone and fires reconnecting.
  2. It retries, waiting a little longer between each try (up to a set limit), so it doesn't flood a bad network.
  3. Each retry first asks the server to resume the session it had. If the drop was short (under about two minutes), the media server still has it, and media never stopped: the call carries on with no renegotiation at all. If it can't be resumed, the SDK builds a fresh media connection and republishes your tracks.
  4. If a retry works, it fires reconnected. Your camera, mic, and screen share are already back. You don't need to redo anything.
  5. If every retry fails, it stops and reports CONNECTION_FAILED. Now your app should offer a "Rejoin" button.

The SDK also recovers when only the media connection fails and the signaling connection is fine (a network that drops UDP mid-call, say): it fires reconnecting, rejoins with a new media connection, and fires reconnected once media flows again. And when the server is restarted for a deploy, clients come back within two seconds, resuming their sessions.

This works the same on every platform.

Turn auto-reconnect on or off

It's on by default, and most apps should leave it on. Turn it off only if you want to handle every drop yourself, for example to send the user back to a lobby screen instead.

createRTCClient({ token, endpoint, autoReconnect: true }); // default

On web, the same hook is room.handleNetworkChange(). Browsers rarely tell a page when the network changes, so the web SDK never calls it on its own.

SettingWhat happens when the network drops
autoReconnect: true (default)You get reconnecting, then reconnected. If all retries fail: disconnected plus an error with code CONNECTION_FAILED.
autoReconnect: falseGoes straight to disconnected. No retries.

Show a "Reconnecting…" banner

Users are much more patient when they can see what's happening. A banner turns "the call froze" into "it's reconnecting".

room.on('reconnecting', () => showBanner('Reconnecting…'));
room.on('reconnected', () => hideBanner());
room.on('error', (error) => {
  if (error.code === 'CONNECTION_FAILED') {
    showError('Lost the connection. Try rejoining.');
  }
});

When the SDK does not reconnect

Some disconnects are on purpose, and reconnecting would be wrong. The disconnected event carries a reason, so your UI can say the right thing:

ReasonWhyWhat to show
CLIENT_INITIATEDYour app called leave().Nothing.
REMOVEDYour backend removed this participant."You were removed from the call." Don't rejoin automatically.
ROOM_CLOSEDThe room was closed."The call has ended."
TOKEN_EXPIREDThe token expired and there was no way to get a new one.Get a fresh token and join again. See Keep long calls alive.
CONNECTION_FAILEDThe network or server failed and every retry did too.A "Rejoin" button.
room.on('disconnected', (reason) => {
  if (reason === 'REMOVED') showNotice('You were removed from the call.');
  else if (reason === 'ROOM_CLOSED') showNotice('The call has ended.');
  else if (reason !== 'CLIENT_INITIATED') showRejoinButton();
});

React Native uses the same Room event.

Keep long calls alive

An RTC token is short-lived. Pass refreshToken and the SDK asks your backend for a new one before the old one expires, then hands it to the live session. There is no reconnect and no glitch. It is also used if a reconnect would otherwise present an expired token.

createRTCClient({
  token,
  endpoint,
  refreshToken: async () => {
    const res = await fetch('/api/rtc-token', { method: 'POST' });
    return (await res.json()).token; // same room and identity as before
  },
});
 
room.on('warning', (warning) => {
  // 'TOKEN_REFRESH_FAILED': refreshToken threw, or the server refused the
  // new token. The call carries on with the old token for now.
  // 'TOKEN_EXPIRING': the token expires soon and there is no refreshToken.
  console.warn(warning.code, warning.message);
});

The new token must be minted for the same room and identity. The server refuses one that isn't, and you get a TOKEN_REFRESH_FAILED warning.

What users see during a reconnect

  • Other people's video and audio stay connected through a short drop. You usually don't need to re-attach anything. If the session couldn't be resumed, each remote track fires trackUnsubscribed and then trackSubscribed again with a new track, so attach the new one.
  • Your own camera, mic, and screen share come back automatically. You don't call enableCamera() again, and nothing is sent twice.
  • If the state goes disconnected → failed, the room can't recover. Join again from scratch.

App lifecycle (React Native)

When the app goes to the background:

  • The call stays connected. A quick app switch shouldn't make someone leave the meeting.
  • Video stops (the OS pauses the camera) and starts again when the user comes back.

Want to leave the call instead? That's up to you:

new Livqeno({
  onAppStateChange: (state) => {
    if (state === 'background') void livqeno.leave(); // your call, not automatic
  },
});

To keep audio playing in the background, see Background Audio.

Show call quality

A connection can stay up but still be bad: choppy video, robotic audio. A quality badge (like a signal-strength icon) tells the user it's their network, not your app.

The media server scores every participant's connection once a second, from what it measures itself: packet loss, jitter, round-trip time, and for someone receiving video, how much bandwidth they have against what their videos need. It pushes the score when it changes, so there's nothing to poll:

room.on('connectionQualityChanged', (participant, quality) => {
  // 'excellent' | 'good' | 'poor' | 'lost'
  if (participant === room.localParticipant) updateQualityBadge(quality);
  else updateTileBadge(participant.identity, quality);
});
 
room.localParticipant.connectionQuality; // the current score, 'unknown' until the first one

This works the same on Web and React Native. React has useConnectionQuality(participant); Flutter has room.connectionQualityChanges and participant.connectionQuality.

Against a server older than this feature the score stays 'unknown' (never a guess). room.getConnectionStats() has the raw numbers either way (see Diagnostics).

The same numbers also reach Livqeno's servers. See them with livqeno connections inspect, or in the Event Catalogue.

Common errors

ErrorCauseFix
CONNECTION_FAILEDEvery reconnect attempt failed.Join again with client.join() / livqeno.join(). Get a fresh token if the old one expired.
TOKEN_EXPIREDThe server ended the session because its token expired.Pass refreshToken so the SDK can renew it.
Reconnect never happensautoReconnect: false is set.Remove it, or handle disconnected yourself and call join() again.

Tips for production

  • Don't write your own retry loop around join(). It will fight the SDK's own reconnects. Wait for failed, then join again.
  • A "Reconnecting…" banner that never goes away usually means your app hit an error during the reconnect. Look for an uncaught error nearby.

Next steps