Skip to main content
Daily provides events and methods for tracking call quality in real time and for running pre-call tests before a user joins. This guide covers the most common patterns for each.

Monitoring quality during a call

Reacting to quality changes

The network-quality-change event fires whenever Daily’s assessment of the local participant’s network changes. This is the recommended way to drive a quality indicator in your UI — no polling required.
call.on('network-quality-change', ({ networkState, networkStateReasons }) => {
  switch (networkState) {
    case 'good':
      showQualityIndicator('good');
      break;
    case 'warning':
      showQualityIndicator('warning');
      console.log('Degraded due to:', networkStateReasons);
      break;
    case 'bad':
      showQualityIndicator('bad');
      notifyUser('Your connection quality is poor.');
      break;
  }
});
networkState is calculated from packet loss, round-trip time, and available outgoing bitrate, averaged over a ~30-second rolling window. When the state is 'warning' or 'bad', networkStateReasons tells you which metrics are responsible — 'sendPacketLoss', 'recvPacketLoss', 'roundTripTime', or 'availableOutgoingBitrate' — which can help you decide how to respond (for example, prompting a user to check their connection vs. proactively reducing video quality).
In Daily Prebuilt, 'warning' triggers automatic bandwidth reduction and 'bad' disables the local camera. In a custom UI you’re responsible for any adaptive behavior.

Getting detailed stats on demand

getNetworkStats() returns a point-in-time snapshot with per-stream bitrates, packet loss, jitter, and RTT — useful if you want to log detailed diagnostics, build an advanced quality panel, or inspect conditions at a specific moment.
const { networkState, networkStateReasons, stats } = await call.getNetworkStats();

if (stats.latest) {
  console.log('Send packet loss:', stats.latest.totalSendPacketLoss);
  console.log('RTT (ms):', stats.latest.networkRoundTripTime);
  console.log('Worst video recv packet loss this session:', stats.worstVideoRecvPacketLoss);
}
Stats in stats.latest are updated approximately every two seconds. The object is empty early in the call before enough data has been collected.
Firefox does not report send-side packet loss or audio jitter, and does not provide availableOutgoingBitrate or networkRoundTripTime.

Handling connection interruptions

The network-connection event fires when a connection is established or interrupted. Every call uses two connections — signaling (call management) and sfu (audio/video) — and both reconnect automatically as long as signaling is alive.
call.on('network-connection', ({ type, event }) => {
  if (event === 'interrupted') {
    if (type === 'signaling') {
      // Participant will be ejected if signaling doesn't recover within ~20s
      showReconnectingBanner();
    } else {
      // SFU interruption — media will pause but reconnect automatically
      showMediaPausedIndicator();
    }
  } else if (event === 'connected') {
    hideReconnectingBanner();
  }
});

Monitoring CPU load

High CPU load can degrade video quality independently of network conditions, particularly on lower-powered devices or in large calls with many incoming video streams. Listen for cpu-load-change to detect this:
call.on('cpu-load-change', ({ cpuLoadState, cpuLoadStateReason }) => {
  if (cpuLoadState === 'high') {
    console.log('High CPU load — reason:', cpuLoadStateReason);

    if (cpuLoadStateReason === 'encode') {
      // Reduce outgoing video quality
      call.updateSendSettings({ video: { maxQuality: 'low' } });
    } else if (cpuLoadStateReason === 'decode') {
      // Reduce number of incoming video streams
      call.updateReceiveSettings({ '*': { video: { layer: 0 } } });
    }
  }
});
cpuLoadStateReason identifies whether encoding, decoding, or scheduling is the bottleneck, so you can target the right response. Use getCpuLoadStats() to poll for per-track decode stats or inspect frame encode/decode times in detail.
CPU load monitoring applies to video calls only.

Testing quality before joining

Running a pre-call test lets you warn users about poor conditions before they enter a call, or initialize bandwidth settings appropriately.

testCallQuality()

testCallQuality() is the recommended pre-call test. It connects to a private Daily room, streams video to Daily’s infrastructure for up to 30 seconds, and returns a verdict based on packet loss, round-trip time, and available bitrate. Call it after preAuth() or startCamera() and before join():
const call = Daily.createCallObject();
await call.preAuth({ url: DAILY_ROOM_URL });

const results = await call.testCallQuality();

switch (results.result) {
  case 'good':
    proceedToJoin();
    break;
  case 'warning':
    showWarning('Your connection may cause choppy audio or video.');
    offerToJoinAnyway();
    break;
  case 'bad':
    showWarning('Your connection is poor. Try a different network.');
    offerToJoinAnyway();
    break;
  case 'failed':
    showError('Could not reach Daily servers. Check your network and try again.');
    break;
  case 'aborted':
    // join() or stopTestCallQuality() was called before data collection finished
    break;
}
The test runs for up to 30 seconds but you can stop it early:
call.stopTestCallQuality();
If Adaptive Bitrate is enabled (the default for 1:1 calls), the test results are used automatically at join time to initialize the highest simulcast layer appropriately — so running the test improves the call start even if the result is 'good'.

Other pre-call tests

Three additional tests are available for more targeted diagnostics:
  • testWebsocketConnectivity() — Checks whether WebSocket connections can be established with Daily’s signaling servers across AWS regions, without requiring a camera or a room. This is the right first check if you’re building for environments where corporate firewalls or VPNs might block WebSockets — a 'failed' result here means the call can’t even get started.
  • testNetworkConnectivity(videoTrack) — Checks whether a TURN connection can be established with Daily’s infrastructure. A step beyond WebSocket connectivity, useful for stricter network environments. Returns a simple 'passed' / 'failed' rather than a quality grade.
  • testPeerToPeerCallQuality(options) — Returns a quality verdict ('good' / 'warning' / 'bad') using TURN servers rather than an actual Daily room session. Runs faster (15 seconds by default) and doesn’t require a room URL, but it measures receive-side metrics rather than send-side, and unlike testCallQuality(), it doesn’t seed Adaptive Bitrate for the subsequent call.

Listening for results via event

If you’re building on top of Daily Prebuilt, it runs testCallQuality() automatically in its prejoin UI. You can read the results without calling the method yourself by listening for the test-completed event:
call.on('test-completed', ({ test, results }) => {
  if (test === 'call-quality') {
    console.log('Pre-call quality result:', results.result);
  }
});

See also