Skip to content
On this pageOPEN
  1. 01Goal
  2. 02Plain-language concept
  3. 03Interactive diagram
  4. 04Behind the scenes
  5. 05Code walkthrough
  6. 06Do & don't
  7. 07Practice
  8. 08Quick quiz
  9. 09Key takeaways
  10. 10Common errors
LESSON 11Advanced30 min read

Network quality & getStats

Measure RTT, jitter, packet loss, bitrate, and frames from getStats, then turn those numbers into user-facing truth.

#getStats#jitter#packet loss#RTT#bitrate
01

Goal

  • 01Read getStats without confusing cumulative counters, current gauges, and rates you must calculate.
  • 02Turn RTT, jitter, loss, bitrate, and frame metrics into a diagnosis rather than a dashboard of noise.
  • 03Emit privacy-conscious client telemetry that can explain real calls after they happen.
02

The concept, plainly

`getStats()` returns a `RTCStatsReport`: a map of typed records for inbound RTP, outbound RTP, candidate pairs, transport, data channels, and more. Each record has an id, type, and timestamp. The report is a snapshot. Most values improve when you compute deltas at a known interval instead of treating cumulative counters as rates.

The selected path comes from the transport's `selectedCandidatePairId` and the pair's local and remote candidate ids. RTT from that pair is the network round trip. It is not the same as the application's request latency, and it does not include jitter-buffer delay. Candidate type tells you whether the path is direct or relayed.

Inbound `packetsLost`, `packetsReceived`, `jitter`, and `framesDecoded` describe what one receiver experienced. Outbound `bytesSent`, `packetsSent`, `framesEncoded`, and `qualityLimitationReason` describe what one sender attempted. They are separate directions and can have very different health. Always label direction and source; unlabeled metrics invite wrong conclusions.

Calculate bitrate as `(bytesNow - bytesBefore) * 8 / elapsedSeconds`, with a monotonic clock and protection against counter resets, reconnects, or switching peer connections. Packet loss rate should normally use deltas over the interval as well as the cumulative ratio; a burst matters more than a lifetime average. Jitter is expressed in seconds in stats, so multiply by 1000 for milliseconds.

Quality is subjective. A 150 ms RTT is fine for data but noticeable in conversation; 2% packet loss may be tolerable for video and terrible for remote control. Combine metrics into a small set of states: good, degraded, poor, disconnected. Include the quality limitation reason. CPU-limited video should not be reported as a network outage.

Telemetry needs context: browser, platform, topology, whether TURN was used, media direction, and the selected codec. Do not upload raw SDP, IP addresses, device labels, or full candidate lists by default. Use short-lived aggregate samples with a session id and explicit retention rules. Metrics without context cannot answer 'why did this call fail?'

03

See it move

PLAY / PAUSE / STEP

Interactive diagram

From stats objects to a decision

sampletransportcountersdelta / timeclassifypathsampledimensionsdecideBROWSERRTCPeerConnectionlive transportPROCESSgetStats()snapshot of recordsNETWORKSelected pairpath + RTTSTORAGERaw countersbytes · packetsPROCESSRatesbitrate · loss deltaPROCESSQuality stategood / degraded / poorSERVERContexttopology · codec · pathSTORAGETelemetrybounded samplesPEERUser actionwarn · adapt · retry
STEP 01 / 081–5 s

Sample on a fixed cadence

Call getStats every one to five seconds for live diagnostics. Faster produces noise; slower misses short bursts. Keep the previous sample per peer generation.

Collect, classify, derive rates, and combine context. A metric is only useful if it changes a decision.

04

Behind the scenes

01

Reports are graphs, not flat rows

Inbound RTP points to a codec and a transport. Candidate pairs point to local and remote candidates. The transport points to the selected pair. Follow these ids to answer questions that a single row cannot: is the selected pair relayed, what codec is actually receiving, and is the timestamp fresh?

02

Cumulative loss hides bursts

A call that lost 1% over ten minutes may have been unusable for thirty seconds in the middle. Sampling deltas gives you the burst. Keep both cumulative and interval views so you can distinguish a chronic bad path from a transient event.

03

Browser stats are not identical

Field availability and timing vary across Chromium, Firefox, and Safari. Some fields are optional or appear only after media flows. Write diagnostics defensively: tolerate missing fields, label undefined values honestly, and test the browsers you actually support rather than assuming Chromium's report shape.

05

Code walkthrough

COPY-READY

Track the selected pair, derive bitrate from byte deltas, and classify a simple quality state. Replace the logging with your telemetry sink.

lib/stats-collector.ts
type Sample = {
at: number;
inboundBytes: number;
outboundBytes: number;
packetsLost: number;
packetsReceived: number;
jitterMs: number | null;
rttMs: number | null;
relayed: boolean;
bitrateIn: number;
bitrateOut: number;
};
export async function collectSample(pc: RTCPeerConnection, previous: Sample | null): Promise<Sample> {
const report = await pc.getStats();
const now = performance.now();
let inboundBytes = 0, outboundBytes = 0;
let packetsLost = 0, packetsReceived = 0, jitterMs: number | null = null;
let pairId: string | undefined;
report.forEach((stat) => {
if (stat.type === "inbound-rtp") {
inboundBytes += stat.bytesReceived ?? 0;
packetsLost += stat.packetsLost ?? 0;
packetsReceived += stat.packetsReceived ?? 0;
if (typeof stat.jitter === "number") jitterMs = stat.jitter * 1000;
}
if (stat.type === "outbound-rtp") outboundBytes += stat.bytesSent ?? 0;
if (stat.type === "transport") pairId = stat.selectedCandidatePairId;
});
const pair = pairId ? report.get(pairId) : undefined;
const local = pair ? report.get(pair.localCandidateId) : undefined;
const remote = pair ? report.get(pair.remoteCandidateId) : undefined;
const elapsed = previous ? Math.max(0.25, (now - previous.at) / 1000) : 0;
return {
at: now,
inboundBytes,
outboundBytes,
packetsLost,
packetsReceived,
jitterMs,
rttMs: pair?.currentRoundTripTime ? pair.currentRoundTripTime * 1000 : null,
relayed: local?.candidateType === "relay" || remote?.candidateType === "relay",
bitrateIn: elapsed ? Math.max(0, (inboundBytes - (previous?.inboundBytes ?? inboundBytes)) * 8 / elapsed) : 0,
bitrateOut: elapsed ? Math.max(0, (outboundBytes - (previous?.outboundBytes ?? outboundBytes)) * 8 / elapsed) : 0,
};
}
export function qualityState(sample: Sample) {
if (sample.rttMs === null) return "connecting";
const loss = sample.packetsReceived + sample.packetsLost > 0
? sample.packetsLost / (sample.packetsReceived + sample.packetsLost)
: 0;
if (sample.rttMs > 400 || loss > 0.08) return "poor";
if (sample.rttMs > 200 || loss > 0.02 || (sample.jitterMs ?? 0) > 60) return "degraded";
return "good";
}
06

Do and don't

Do

  • Label direction and source

    Inbound loss is the receiver's view; outbound target bitrate is the sender's intent. They answer different questions.

  • Use deltas for rates

    Bitrate and interval loss require two samples and elapsed time. Store the previous sample per connection id.

  • Include quality limitation

    CPU-limited video is not a network problem. This one field saves hours of misplaced debugging.

  • Keep telemetry small and contextual

    Aggregate metrics, strip addresses and labels, include topology and browser, and define a retention window.

Don't

  • Don't treat cumulative bytes as speed

    A large bytesSent value only means the call has been running. Compute a rate over a known interval.

  • Don't compare directions

    A healthy outbound stream does not prove the inbound path is healthy. Sample both separately.

  • Don't upload raw getStats dumps

    They can contain IP addresses, candidate details, and device-adjacent metadata. Aggregate and redact.

  • Don't show a number without a decision

    If a metric does not change support, adaptation, or user guidance, it is decoration.

07

Practice

Hands-on lab

Read a live report

The lab parses real getStats into RTT, jitter, loss, bitrate, frame size, FPS, packets, bytes, and the selected pair. Use it as your reference implementation.

  1. 1Connect loopback and record three samples a second apart. Explain why bitrate changes while bytes only grow.
  2. 2Find the selected pair and decide whether it is direct or relayed.
  3. 3Set a candidate filter that prevents a path and observe which fields stop updating.
  4. 4After disconnecting, explain which counters would be misleading if carried into the next session.
Open the getStats lab

Solo? Loopback is enough. Both peers are local, so the network numbers are boring, but the graph structure, counters, and path selection are real.

08

Quick quiz

Knowledge check

0 of 5 answered

0%
  1. 01

    Which value is cumulative rather than a current rate?

  2. 02

    How do you find which candidate pair carries media?

  3. 03

    qualityLimitationReason is 'cpu'. What does that suggest?

  4. 04

    Why is a lifetime average packet-loss rate insufficient?

  5. 05

    Which is the safest telemetry choice?

Pick one answer per question to see instant feedback.
09

Key takeaways

  1. getStats is a graph of records; read the selected pair and the direction you care about.01
  2. Counters need deltas, and deltas need a monotonic clock and a generation check.02
  3. qualityLimitationReason separates CPU problems from network problems.03
  4. Context and privacy are part of telemetry design, not an afterthought.04
10

Common errors and fixes

Terms in this lesson

Done with this lesson?

Mark it complete, then keep the thread.