![Henrik Boström](https://web.dev/images/authors/henrikbostrom.jpg) Henrik Boström

<br />

The legacy `getStats()` WebRTC API will be removed in Chrome 117, therefore apps using it will need to migrate to the standard API. This article explains how to migrate your code, and what to do if you need more time to make this change.

Historically there have been two competing versions of the WebRTC `getStats()` API. The legacy getStats() API, that pre-dates the standardization process and takes a callback argument, and the standardized and widely supported API which returns a promise.

The standard API is more feature rich and has well-defined metrics publicly documented in the W3C specification [Identifiers for WebRTC's Statistics API](https://w3c.github.io/webrtc-stats/). The specification includes descriptions of each metric listed in this guide and many more.

From Chrome 117 the legacy `getStats()` API will throw an exception in the Stable release channel (the exception throwing will be gradually rolled out). Follow this guide to ease your transition to the standard API.

> [!NOTE]
> **Note:** You can compare legacy and standard metrics in real-time while your app is running by going to `chrome://webrtc-internals/` in a separate tab, then selecting **legacy** or **standard** metrics using the drop-down table.

## Legacy versus standard stats types

The complete list of standard stats types can be found by looking at the [RTCStatsType](https://w3c.github.io/webrtc-stats/#rtctatstype-*) enum in the specification. This includes which stats dictionary definition describes the metrics collected for each type.

The stats objects all have an id attribute which uniquely identifies the underlying object across multiple `getStats()` calls. The same object will have the same ID every time the method is called. This is useful for calculating the rate of change of metrics (there's an example in the next section). The IDs also form relationships of references. For example the `outbound-rtp` stats object references the associated `media-source` stats object via the `outbound-rtp.mediaSourceId` attribute. If you draw all `...Id` relationships you get a graph.

The legacy API has the following stats types, corresponding to the standard types as follows:

| **Legacy type** | **Standard type** |
|---|---|
| ` ssrc ` | Represents an RTP stream and metrics about the associated `MediaStreamTrack`. <br /> <br /> The standard types for this are `inbound-rtp` (for receive RTP streams and its associated remote `MediaStreamTrack`), `outbound-rtp` (for send RTP streams) and `media-source` (for local `MediaStreamTrack` metrics associated with a send RTP stream). RTP stream metrics also contain information about the encoder or decoder used by the RTP stream. |
| ` VideoBwe ` | Bandwidth estimation metrics, target bitrate, encoder bitrate and actual bitrate. These types of metrics are part of the RTP metrics (`outbound-rtp` and `inbound-rtp`) and ICE candidate pair metrics (`candidate-pair`). |
| ` googComponent ` | Represents the transport (ICE and DTLS). The standard version is `transport`. |
| ` localcandidate and remotecandidate ` | Represents an ICE candidate. The standard version is `local-candidate` and `remote-candidate`. |
| ` googCandidatePair ` | Represents an ICE candidate pair, which is a pairing of a local and a remote candidate. The standard version is `candidate-pair`. |
| ` googCertificate ` | Represents a certificate used by the DTLS transport. The standard version is `certificate`. |
| ` googLibjingleSession ` | Represents the `RTCPeerConnection`. While its contents does not map to anything in the standard, the standard does have a type associated with the `RTCPeerConnection`: `peer-connection`. |
| Missing from legacy API | These stats types have been added to the standard API that don't have any corresponding legacy type: - `codec`: A codec that is currently being used by an RTP stream, either for encoding or decoding. This is a subset of the codecs that have been negotiated in the SDP. <!-- --> - `remote-inbound-rtp`: A remote endpoint's inbound RTP stream corresponding to an outbound RTP stream that this endpoint is sending (`outbound-rtp`). It is measured at the remote endpoint and reported in an RTCP Receiver Report (RR) or RTCP Extended Report (XR). <!-- --> - `remote-outbound-rtp`: A remote endpoint's outbound RTP stream corresponding to an inbound RTP stream that this endpoint is receiving (`inbound-rtp`). It is measured at the remote endpoint and reported in an RTCP Sender Report (SR). <!-- --> - `media-playout`: Metrics about the playout of a remote `MediaStreamTrack` associated with an inbound RTP stream (`inbound-rtp`). <!-- --> - `data-channel`: Represents an `RTCDataChannel`. |

## Legacy to standard metrics mapping

This mapping is aimed to help developers find which legacy metric corresponds to which standard metric, but note that the corresponding metric may use different units or be expressed as a total counter rather than an instantaneous value. Refer to the specification for metric definitions.  

The standard API prefers exposing total counters rather than rates. This means that to get the corresponding rate (for example, bitrate) as in the legacy API, the app must calculate the average rate by taking the delta between two `getStats()` calls. For example:

```javascript
// Periodically (e.g. every second or every 10 seconds)...
const currReport = await pc.getStats();
// Calculate bitrate since the last getStats() call.
// Handling of undefined is omitted for clarity.
const currOutboundRtp = currReport.values().find(s => s.type == 'outbound-rtp');
const prevOutboundRtp = prevReport.get(currOutboundRtp.id);
const deltaBits = (currOutboundRtp.bytesSent - prevOutboundRtp.bytesSent) * 8;
const deltaSeconds = (currOutboundRtp.timestamp - prevOutboundRtp.timestamp) / 1000;
logBitrateMeasurement(deltaBits / deltaSeconds);
// Remember the report for next time.
prevReport = currReport;
```

Having to calculate rates and averages yourself like this may seem like a cumbersome additional step but it has the upside of allowing you to get averages over any desired time interval. Calling the standard API less often than you may otherwise have had to do with the legacy API has some performance benefits.

| Legacy metric `googCertificate` | Standard correspondence `certificate` |
|---|---|
| ` .googFingerprint ` | ` .fingerprint ` |
| ` .googFingerprintAlgorithm ` | ` .fingerprintAlgorithm ` |
| ` .googDerBase64 ` | ` .base64Certificate ` |

| Legacy metric `googComponent` | Standard correspondence `transport` |
|---|---|
| ` .localCertificateId ` | ` .localCertificateId ` |
| ` .remoteCertificateId ` | ` .remoteCertificateId ` |
| ` .selectedCandidatePairId ` | ` .selectedCandidatePairId ` |
| ` .dtlsCipher ` | ` .dtlsCipher ` |
| ` .srtpCipher ` | ` .srtpCipher ` |

| Legacy metric `localcandidate` | Standard correspondence `local-candidate` or `candidate-pair` |
|---|---|
| ` .stunKeepaliveRequestsSent ` | `candidate-pair.requestsSent` (reverse lookup `candidate-pair` via `candidate-pair.localCandidateId`) |
| ` .portNumber ` | ` local-candidate.port ` |
| ` .networkType ` | ` local-candidate.networkType ` |
| ` .ipAddress ` | ` local-candidate.address ` |
| ` .stunKeepaliveResponsesReceived ` | ` candidate-pair.responsesReceived ` |
| ` .stunKeepaliveRttTotal ` | ` candidate-pair.totalRoundTripTime ` |
| ` .transport ` | ` local-candidate.protocol ` |
| ` .candidateType ` | ` local-candidate.candidateType ` |
| ` .priority ` | ` local-candidate.priority ` |

| Legacy metric `remotecandidate` | Standard correspondence `remote-candidate` |
|---|---|
| Same as `localcandidate` above. | Same as `local-candidate` above. |

| Legacy metric `googCandidatePair` | Standard correspondence `candidate-pair` |
|---|---|
| ` .responsesSent ` | ` candidate-pair.responsesSent ` |
| ` .requestsReceived ` | ` candidate-pair.requestsReceived ` |
| ` .googRemoteCandidateType ` | `remote-candidate.candidateType` (lookup `remote-candidate` via `candidate-pair.remoteCandidateId`) |
| ` .googReadable ` | `googReadable` is a boolean reflecting whether or not we've recently incremented `candidate-pair.requestsReceived` or `candidate-pair.responsesReceived` |
| ` .googLocalAddress ` | `local-candidate.address` (lookup `local-candidate` via `candidate-pair.localCandidateId`) |
| ` .consentRequestsSent ` | ` candidate-pair.consentRequestsSent ` |
| ` .googTransportType ` | Same as `local-candidate.protocol` and `remote-candidate.protocol`. |
| ` .googChannelId ` | ` candidate-pair.transportId ` |
| ` .googLocalCandidateType ` | ` local-candidate.candidateType ` |
| ` .googWritable ` | `googWritable` is a boolean reflecting whether or not we've recently incremented `candidate-pair.responsesReceived` |
| ` .googRemoteAddress ` | ` remote-candidate.address ` |
| ` .googRtt ` | ` candidate-pair.currentRoundTripTime ` |
| ` .googActiveConnection ` | The *active connection* refers to the candidate pair that is currently selected by the transport, such as where `candidate-pair.id == transport.selectedCandidatePairId` |
| ` .packetsDiscardedOnSend ` | ` candidate-pair.packetsDiscardedOnSend ` |
| ` .bytesReceived ` | ` candidate-pair.bytesReceived ` |
| ` .responsesReceived ` | ` candidate-pair.responsesReceived ` |
| ` .remoteCandidateId ` | ` candidate-pair.remoteCandidateId ` |
| ` .localCandidateId ` | ` candidate-pair.localCandidateId ` |
| ` .bytesSent ` | ` candidate-pair.bytesSent ` |
| ` .packetsSent ` | ` candidate-pair.packetsSent ` |
| ` .bytesReceived ` | ` candidate-pair.bytesReceived ` |
| ` .bytesReceived ` | ` candidate-pair.bytesReceived ` |

| Legacy metric `ssrc` | Standard correspondence `inbound-rtp`, `outbound-rtp`, `media-source` |
|---|---|
| ` .audioInputLevel ` | `media-source.audioLevel`. The legacy metric is in range \[0..32768\] but the standard metrc is in range \[0..1\]. |
| ` .audioOutputLevel ` | `inbound-rtp.audioLevel`. The legacy metric is in range \[0..32768\] but the standard metrc is in range \[0..1\]. |
| ` .packetsLost ` | ` inbound-rtp.packetsLost ` |
| ` .googTrackId ` | `media-source.trackIdentifier` for local `MediaStreamTrack`s and `inbound-rtp.trackIdentifier` for remote `MediaStreamTrack`s |
| ` .googRtt ` | `remote-inbound-rtp.roundTripTime` (see `outbound-rtp.remoteId`) |
| ` .googEchoCancellationReturnLossEnhancement ` | ` inbound-rtp.echoReturnLossEnhancement ` |
| ` .googCodecName ` | The codec name is the subtype of the "type/subtype" mime type, `codec.mimeType` (see `inbound-rtp.codecId` and `outbound-rtp.codecId`) |
| ` .transportId ` | `inbound-rtp.transportId` and `outbound-rtp.transportId` |
| ` .mediaType ` | `inbound-rtp.kind` and `outbound-rtp.kind` or `media-source.kind` |
| ` .googEchoCancellationReturnLoss ` | ` inbound-rtp.echoReturnLoss ` |
| ` .totalAudioEnergy ` | `inbound-rtp.totalAudioEnergy` and `media-source.totalAudioEnergy` |
| ` ssrc.totalSamplesDuration ` | `inbound-rtp.totalSamplesDuration` and `media-source.totalSamplesDuration` |
| ` .ssrc ` | `inbound-rtp.ssrc` and `outbound-rtp.ssrc` |
| ` .googJitterReceived ` | ` inbound-rtp.jitter ` |
| ` .packetsSent ` | ` outbound-rtp.packetsSent ` |
| ` .bytesSent ` | ` outbound-rtp.bytesSent ` |
| ` .googContentType ` | `inbound-rtp.contentType` and `outbound-rtp.contentType` |
| ` .googFrameWidthInput ` | ` media-source.width ` |
| ` .googFrameHeightInput ` | ` media-source.height ` |
| ` .googFrameRateInput ` | ` media-source.framesPerSecond ` |
| ` .googFrameWidthSent ` | ` outbound-rtp.frameWidth ` |
| ` .googFrameHeightSent ` | ` outbound-rtp.frameHeight ` |
| ` .googFrameRateSent ` | While the send FPS is the rate of change of `outbound-rtp.framesSent`, this is actually implemented as `outbound-rtp.framesPerSecond` which is encoding FPS. |
| ` .googFrameWidthReceived ` | ` inbound-rtp.frameWidth ` |
| ` .googFrameHeightReceived ` | ` inbound-rtp.frameHeight ` |
| ` .googFrameRateDecoded ` | The rate of change of `inbound-rtp.framesDecoded` |
| ` .googFrameRateOutput ` | The rate of change of `inbound-rtp.framesDecoded` - `inbound-rtp.framesDropped` |
| ` .hugeFramesSent ` | ` outbound-rtp.hugeFramesSent ` |
| ` .qpSum ` | `inbound-rtp.qpSum` and `outbound-rtp.qpSum` |
| ` .framesEncoded ` | ` outbound-rtp.framesEncoded ` |
| ` .googAvgEncodeMs ` | `outbound-rtp.totalEncodeTime` / `outbound-rtp.framesEncoded` |
| ` .codecImplementationName ` | `inbound-rtp.decoderImplementation` and `outbound-rtp.encoderImplementation` |
| ` .googCpuLimitedResolution ` | True if `outbound-rtp.qualityLimitationReason == "cpu"` |
| ` .googBandwidthLimitedResolution ` | True if `outbound-rtp.qualityLimitationReason == "bandwidth"` |
| ` .googAdaptationChanges ` | The legacy metric counts the number of times resolution or frame rate changed for `qualityLimitationReason` related reasons. This could be deduced from other metrics (e.g. send resolution or frame rate being different from source resolution or frame rate), but the duration that we have been limited, `outbound-rtp.qualityLimitationDurations`, may be more useful than how frequently resolution or frame rate changed was reconfigured. |
| ` .googNacksReceived ` | ` inbound-rtp.nackCount ` |
| ` .googNacksSent ` | ` inbound-rtp.nackCount ` |
| ` .googPlisReceived ` | ` inbound-rtp.pliCount ` |
| ` .googPlisSent ` | ` inbound-rtp.pliCount ` |
| ` .googFirsReceived ` | ` inbound-rtp.firCount ` |
| ` .googFirsSent ` | ` inbound-rtp.firCount ` |
| ` .googSecondaryDecodedRate ` | The recent ratio of packets containing error correction: `inbound-rtp.fecPacketsReceived` - `inbound-rtp.fecPacketsDiscarded` |
| ` .packetsReceived ` | ` inbound-rtp.packetsReceived ` |
| ` .googJitterBufferMs ` | ` inbound-rtp.jitterBufferDelay` / `inbound-rtp.jitterBufferEmittedCount ` |
| `.googTargetDelayMs` (video) | ` inbound-rtp.jitterBufferTargetDelay` / `inbound-rtp.jitterBufferEmittedCount ` |
| `.googPreferredJitterBufferMs` (audio) | ` inbound-rtp.jitterBufferTargetDelay` / `inbound-rtp.jitterBufferEmittedCount ` |
| ` .googExpandRate ` | The recent ratio of concealed samples: `inbound-rtp.concealedSamples` / `inbound-rtp.totalSamplesReceived` |
| ` .googSpeechExpandRate ` | The recent ratio of concealed samples while the stream was not silent: of (`inbound-rtp.concealedSamples` - `inbound-rtp.silentConcealedSamples`) / `inbound-rtp.concealedSamples` |
| ` .googAccelerateRate ` | The recent ratio of samples that were discarded in order to accelerate playout speed: `inbound-rtp.removedSamplesForAcceleration` / `inbound-rtp.totalSamplesReceived` |
| ` .googPreemptiveExpandRate ` | The recent ratio of samples that were synthesized in order to decelerate playout speed: `inbound-rtp.insertedSamplesForDeceleration` / `inbound-rtp.totalSamplesReceived` |
| ` .googSecondaryDiscardedRate ` | ` inbound-rtp.fecPacketsDiscarded ` |
| ` .bytesReceived ` | ` inbound-rtp.bytesReceived ` |
| ` s.googCurrentDelayMs ` | `inbound-rtp.jitterBufferDelay` + media-playout.totalPlayoutDelay |
| ` .googDecodeMs ` | `inbound-rtp.totalDecodeTime` / `inbound-rtp.framesDecoded` |
| ` .googTimingFrameInfo ` | The only remaining goog-metric. `inbound-rtp.googTimingFrameInfo` |
| ` .framesDecoded ` | ` inbound-rtp.framesDecoded ` |

| Legacy metric `VideoBwe` | Standard correspondence `outbound-rtp` and `candidate-pair` |
|---|---|
| ` .googTargetEncBitrate ` | `outbound-rtp.targetBitrate` as an instantaneous value or `outbound-rtp.totalEncodedBytesTarget` / `outbound-rtp.framesEncoded` as an average |
| ` .googActualEncBitrate ` | The bytes produced by the encoder are the payload bytes, excluding retransmissions: the rate of change of `outbound-rtp.bytesSent` - `outbound-rtp.retransmittedBytesSent` |
| ` .googBucketDelay ` | `outbound-rtp.totalPacketSendDelay` / `outbound-rtp.packetsSent ` |
| ` .googTransmitBitrate ` | The rate of change of `outbound-rtp.headerBytesSent` + `outbound-rtp.bytesSent` for per-RTP stream bitrate, `candidate-pair.bytesSent` for per-ICE candidate bitrate or `transport.bytesSent` for per-transport bitrate |
| ` .googRetransmitBitrate ` | The range of change of `outbound-rtp.retransmittedBytesSent` |
| ` .googAvailableSendBandwidth ` | ` candidate-pair.availableOutgoingBitrate ` |
| ` .googAvailableReceiveBandwidth ` | ` candidate-pair.availableIncomingBitrate ` |

## The standard API is simulcast-aware

If you use simulcast you may have noticed that the legacy API only reports a single SSRC even when you're using simulcast to send (for example) three RTP streams over three separate SSRCs.

The standard API does not share this limitation and will return three `outbound-rtp` stats objects, one for each of the SSRCs. This means that you can analyze each RTP stream individually, but it also means that to obtain the total bitrate of all RTP send streams you'll need to aggregate them yourself.

SVC streams or RTP streams with multiple spatial layers configured via the `scalabilityMode` API on the other hand still show up as a single `outbound-rtp` because these are sent over a single SSRC.

## If you need more time for migration

When the legacy API is removed in Chrome 117, using it will generate an exception. If you are unable to migrate your code in time, the [origin trial for RTCPeerConnection callback-based getStats() API](https://developer.chrome.com/origintrials#/view_trial/3633278999381147649) gives registered websites more time to migrate. With an origin trial token, the legacy getStats() API may continue to be used until Chrome 121.