Blob: firmware/vendor/str0m/docs/BWE.md
Bandwidth Estimation (BWE)
str0m implements Google Congestion Control (GCC) bandwidth estimation based on Transport-Wide Congestion Control (TWCC) feedback. This document describes the architecture, components, and implementation details of the BWE system.
WebRTC revision
This git revision we have aligned the str0m impl to.
Revision: 2bc24d44be71c186ee0756725abaed7015fbc8bc
Date: Sat Jan 10 00:02:32 2026 -0800
How It Works: High-Level Overview
Bandwidth estimation continuously measures how much data you can send without causing network congestion. It combines three complementary detection methods:
Delay-Based Detection (Primary)
Monitors packet arrival timing. When packets arrive later than expected (queuing delay increasing), the network is congested and we reduce sending rate. This is the primary congestion signal, running continuously on every TWCC feedback report.
Loss-Based Detection (Safety Net)
Tracks packet loss. When packets are being dropped, the network is severely congested. This acts as a safety cap on the delay-based estimate, only reducing the estimate when loss exceeds the link's inherent loss rate. It prevents over-estimation when delay signals are unclear.
Probe-Based Discovery (Active Measurement)
Periodically sends bursts of packets at higher rates to test if more bandwidth is available. Probes provide ground truth measurements that help the system ramp up quickly when conditions improve. They're used:
- At startup to quickly find initial capacity
- When the estimate increases significantly (validating the growth)
- When application demand exceeds current estimate (exploring headroom)
- During ALR to rediscover capacity
Application Limited Region (ALR)
ALR occurs when your application sends less than 65% of available capacity – for example, during silence suppression in audio, or when video is paused. This is important because:
Loss patterns differ: During normal operation, loss indicates congestion. During ALR, loss could be unrelated to your sending rate since you're barely using the network. Without ALR awareness, the loss controller might incorrectly reduce estimates based on irrelevant loss.
Capacity discovery: When operating in ALR, we don't know the true network capacity because we're not pushing against it. The ALR detector tracks this state and provides it to probe control and loss controller for appropriate behavior.
Accurate bounds: Successful ALR probes provide proven capacity measurements. The loss controller uses these as upper bounds during ALR, preventing it from estimating beyond what we've actually demonstrated the network can handle.
Without ALR integration, the BWE system treats application-limited scenarios the same as network-limited ones, leading to incorrect estimates and slower adaptation when application demand resumes.
Signal Flow Diagram
Signal Types:
- 🔴 Red (thick): Core BWE estimates (delay-based, loss-based)
- 🔵 Blue (thick): Application/Network output (final estimate, packet transmission)
- 🟣 Purple: Probe measurements (ALR probe results)
- 🟠 Orange (thick): ALR capacity constraint (proven capacity bound)
- ⚫ Black: Internal/control signals (TWCC feedback, configuration)
Component Overview
SendSideBandwidthEstimator
Location: src/bwe/mod.rs
WebRTC: modules/congestion_controller/goog_cc/send_side_bandwidth_estimation.cc
The main orchestrator that coordinates all BWE components. It receives
TWCC feedback reports and produces bandwidth estimates by merging inputs
from the delay controller, loss controller, and probe system. When TWCC
feedback arrives, it forwards the acknowledged packets to both the delay
and loss controllers, then combines their estimates to produce a final
bandwidth estimate. This estimate is emitted to the application as an
Event::EgressBitrateEstimate event and flows to the pacer control to
adjust pacing and padding rates.
The estimator also manages the probe lifecycle, creating probe cluster
configurations via maybe_create_probe() when bandwidth discovery is
needed, and tracking when probes complete. It monitors for overuse
conditions via is_overusing() which influences both probe scheduling and
pacer behavior. Throughout operation, it tracks ALR (Application Limited
Region) state by forwarding information to probe control and loss
controller.
Delay Controller
Location: src/bwe/delay/control.rs
WebRTC: modules/congestion_controller/goog_cc/delay_based_bwe.cc
Implements delay-based bandwidth estimation by analyzing packet arrival time variations. This is the primary congestion detection mechanism.
How it works:
- Groups packets into arrival groups (via
ArrivalGroupAccumulator) - Calculates inter-group delay variations
- Feeds delay variations to trendline estimator
- Maintains smoothed RTT using EWMA with α = 1/8 (RFC 6298), matching WebRTC's approach where each controller calculates its own smoothed RTT rather than relying on externally-provided values
- Uses rate control (AIMD) to adjust bandwidth estimate at regular
intervals (
UPDATE_INTERVAL= 25ms) - Invalidates the trendline hypothesis if no TWCC feedback received for
more than 2× smoothed RTT (capped at
MAX_TWCC_GAP= 500ms)
The controller produces a delay-based bandwidth estimate along with an overuse state signal, both of which flow to the SendSideBandwidthEstimator for final bandwidth determination.
Arrival Group Accumulator
Location: src/bwe/delay/arrival_group.rs
WebRTC: modules/remote_bitrate_estimator/inter_arrival.cc
Groups packets that were sent close together in time, based on the principle that delay variation between groups is more meaningful than between individual packets. Individual packet delays are noisy due to random jitter, but groups sent together experience similar network conditions, making their delay delta a cleaner congestion signal.
Packets sent within 5ms (SEND_TIME_GROUP_LENGTH) belong to the same
group, creating natural clusters that reflect burst transmission patterns.
The accumulator skips out-of-order packets (by send time) to maintain
monotonic progression, ensuring we compare groups in the order they were
actually sent. It uses a 5ms inter-arrival threshold
(BURST_TIME_INTERVAL) for burst detection. To prevent excessively large
groups during high-bitrate streaming, it caps group duration at 100ms
(MAX_BURST_DURATION), ensuring we don't wait too long between congestion
measurements.
Each completed group pair produces an InterGroupDelayDelta containing
the time between sending the two groups (send_delta), the time between
receiving them (arrival_delta), and the timestamp of the last remote
receive time for analysis. These delta measurements flow to the
DelayController and ultimately to the TrendlineEstimator for congestion
detection.
Trendline Estimator
Location: src/bwe/delay/trendline.rs
WebRTC: modules/congestion_controller/goog_cc/trendline_estimator.cc
Detects bandwidth usage patterns by analyzing the trend in delay
variations over time. The estimator maintains a sliding window of delay
observations (default 20 packets) and computes smoothed delay using
exponential smoothing with a coefficient of 0.9 (SMOOTHING_COEF), which
filters out measurement noise while remaining responsive to real changes. It
then fits a linear regression to the smoothed delay versus time,
producing a trend value that indicates whether queuing delay is increasing
(potential congestion) or decreasing (available capacity).
The raw trend is amplified by multiplying it by both a gain factor of 4.0
(THRESHOLD_GAIN) and the number of observations (capped at 60). This
amplification ensures weak but consistent trends trigger congestion
detection—a small steady increase is as concerning as a large sudden spike.
The modified trend is compared against an adaptive threshold that adjusts
to the network's baseline behavior, accommodating paths with naturally
higher jitter. The threshold starts at 12.5ms
(OVER_USE_THRESHOLD_DEFAULT_MS) and ranges between 6ms and 600ms,
adjusting at different rates depending on whether the trend is within
bounds (faster decrease with K_DOWN = 0.039 to quickly adapt to stable
networks) or exceeding them (slower increase with K_UP = 0.0087 to avoid
masking real congestion).
When the modified trend exceeds the threshold and remains elevated for more
than 10ms (OVER_USE_TIME_THRESHOLD), the estimator declares an Overuse
state, signaling congestion. The 10ms requirement prevents reacting to
transient spikes that don't represent sustained congestion. A negative
trend below the threshold indicates Underuse (available capacity), while
values within the threshold indicate Normal operation. This BandwidthUsage
hypothesis flows to the DelayController and ultimately drives the rate
control AIMD algorithm.
Rate Control (AIMD)
Location: src/bwe/delay/rate_control.rs
WebRTC: modules/remote_bitrate_estimator/aimd_rate_control.cc
Implements the Additive Increase Multiplicative Decrease algorithm that translates congestion signals into bandwidth adjustments. The controller operates in three states: Increase (estimate is growing), Hold (waiting after a decrease before increasing again), and Decrease (actively reducing due to overuse).
When overuse is detected, the controller immediately drops the estimate to
85% (BETA = 0.85) of the currently observed bitrate and transitions to
Hold state. This multiplicative decrease provides rapid backoff from
congestion—the 15% reduction is aggressive enough to quickly relieve queue
buildup while conservative enough to avoid unnecessary underutilization.
The controller remains in Hold state for a period before allowing increases
again, implementing a guard time called TimeToReduceFurther (1 RTT clamped
between 10-200ms). This waiting period ensures the network has time to
drain queues and stabilize before we attempt to grow again.
During the Increase state, the controller's behavior depends on
convergence proximity. When the estimate is within 3 standard deviations
of the observed average bitrate, it switches to additive increase mode,
growing cautiously near the perceived capacity to avoid overshooting into
congestion. Far from convergence, it uses multiplicative increase with a
coefficient of 1.08 raised to time (MULTIPLICATIVE_INCREASE_COEF),
allowing faster exploration of available bandwidth when we're well below
capacity. Regardless of mode, increases are capped at 1.5× the observed
throughput (MAX_ESTIMATE_RATIO) to prevent runaway growth beyond what the
network can actually deliver.
The resulting estimated bitrate flows back to the DelayController, which forwards it to the SendSideBandwidthEstimator for final bandwidth determination.
Acked Bitrate Estimator
Location: src/bwe/acked_bitrate_estimator.rs
WebRTC: modules/congestion_controller/goog_cc/acknowledged_bitrate_estimator.cc
Calculates the observed throughput by tracking acknowledged packets from
TWCC reports within a sliding time window. The estimator uses different
window sizes depending on initialization state: a 500ms window
(INITIAL_BITRATE_WINDOW) for the first estimate to establish a stable
baseline and avoid overreacting to startup transients, then switches to a
150ms window (BITRATE_WINDOW) for subsequent estimates to provide faster
response to changing network conditions.
The calculation employs Bayesian estimation to smooth results and handle
variability, with special handling for small samples (less than 2000 bytes,
SMALL_SAMPLE_THRESHOLD) where statistical noise could skew the
estimate—small samples receive extra smoothing to prevent erratic jumps. To
prevent unrealistic low values during sparse feedback periods (like
silence suppression), it enforces a minimum estimate floor of 40 kbps
(ESTIMATE_FLOOR).
This instantaneous bitrate measurement serves as ground truth for both the DelayController and LossController, providing the "observed average" that rate control uses to calibrate its estimates and detect convergence.
Loss Controller
Location: src/bwe/loss_controller.rs
WebRTC: modules/congestion_controller/goog_cc/loss_based_bwe_v2.cc
Provides loss-based bandwidth estimation using Maximum Likelihood Estimation to distinguish between inherent packet loss (characteristic of the network path—WiFi and cellular networks naturally lose packets even without congestion) and congestion-induced loss (packets dropped because queues are full). The controller operates in three states: DelayBased (deferring to the delay controller), Decreasing (actively limiting bandwidth due to detected loss-based congestion), and Increasing (recovering after loss conditions improve).
The controller estimates the inherent loss rate of the link over an observation window, establishing a baseline for expected packet loss. When observed loss exceeds this inherent rate, it signals congestion and reduces the bandwidth estimate accordingly. The controller includes a Hold mechanism to prevent rapid ramp-up immediately after loss events—loss often indicates we've already exceeded capacity, so we need time to verify the network has recovered before growing again.
During the startup phase (first 2 seconds) when no loss has occurred, the controller defers entirely to the delay-based estimate since we don't have enough data for loss statistics. After a successful probe completes, it adopts the probe result immediately, trusting the active measurement over passive observation—probes provide ground truth about what the network can handle. The controller only ever reduces estimates, acting as a safety cap on the delay-based estimate rather than competing with it. This design prevents the loss controller from causing premature rate reductions based on transient loss while still catching cases where delay signals miss congestion.
The controller integrates with ALR detection and link capacity tracking. When in ALR, it uses proven link capacity from successful ALR probes as an upper bound on estimates, preventing overestimation in application-limited scenarios. When transitioning into or out of ALR, the controller resets its observation window to avoid mixing traffic patterns from different network utilization regimes, ensuring accurate loss characterization for each operating mode.
The loss-based bandwidth estimate flows to the SendSideBandwidthEstimator where it's merged with the delay-based estimate, along with the loss controller's state information.
Probe System
The probe system consists of three components working together to discover available bandwidth.
Probe Control
Location: src/bwe/probe/control.rs
WebRTC: modules/congestion_controller/goog_cc/probe_controller.cc
Decides when and at what rate to send probe clusters to discover available
bandwidth. The controller implements WebRTC's full state machine with three
states: Init (startup), WaitingForProbingResult (active probing with
potential for further probes), and ProbingComplete (idle, monitoring for
triggers). The state machine uses a 1-second timeout
(MAX_WAITING_TIME_FOR_PROBING_RESULT) when waiting for probe results.
In str0m, max_bitrate represents the application's desired sending rate
(equivalent to WebRTC's max_total_allocated_bitrate), not a hard cap.
Since str0m doesn't track per-stream bitrates, WebRTC's separate
max_bitrate (hard cap) and max_total_allocated_bitrate (allocation sum)
concepts are unified into this single value.
Probing Triggers:
Initial Exponential Probing: On startup or when BWE becomes active (
on_bwe_active()), sends two probe clusters immediately at 3× and 6× the start bitrate (first_exponential_probe_scale= 3.0,second_exponential_probe_scale= 6.0). These use standard probe parameters (15ms duration, 2ms min delta between bursts). The exponential scaling allows rapid capacity discovery—if the network can handle 6×, we learn that immediately rather than slowly ramping up over many seconds.Further Probing: When waiting for probe results and the measured bitrate exceeds 70% of the last probe rate (
further_probe_threshold= 0.7), automatically triggers another probe at 2× the measured rate (further_exponential_probe_scale= 2.0). This enables exponential capacity discovery when the network can support it—if we successfully sent at X and are receiving at >0.7X, the link isn't saturated and we should explore higher rates.Allocation Probing: When in ALR and
estimated_bitrate < max_bitrate(application's desired bitrate exceeds current estimate), probes at 1× and 2× max_bitrate (first_allocation_probe_scale= 1.0,second_allocation_probe_scale= 2.0), limited by 2× current BWE estimate (allocation_probe_limit_by_current_scale= 2.0). Triggers immediately upon entering ALR (when estimate is below max_bitrate) and periodically every 5 seconds (MIN_TIME_BETWEEN_ALR_PROBES) while remaining in ALR with headroom available. This level-triggered approach ensures that whenever the application wants more bandwidth than currently available, the system actively probes to discover if the network can support it—whether from initial ALR entry, network improvements during ALR, or sustained application demand exceeding the estimate.Large-Drop Recovery: After a bitrate drop to 66% or below (
BITRATE_DROP_THRESHOLD), if the drop occurred within 5 seconds (BITRATE_DROP_TIMEOUT) and at least 5 seconds have passed since the last recovery probe (MIN_TIME_BETWEEN_ALR_PROBES), probes at 85% of the pre-drop rate (PROBE_FRACTION_AFTER_DROP). Only active when in ALR or within 3 seconds after leaving ALR (ALR_ENDED_TIMEOUT). This recovers from temporary network glitches—if we suddenly dropped due to transient congestion but are now in ALR (low utilization), it's worth testing if we can return to the higher rate.Stagnant Estimate Probing (str0m addition): When the estimate has remained unchanged (within 5%) for 15+ seconds (
STAGNANT_ESTIMATE_DURATION) and there's unmet demand (desired > estimate), probes at 2× current estimate (STAGNANT_PROBE_SCALE). Rate-limited to once per 15 seconds (MIN_TIME_BETWEEN_STAGNANT_PROBES). Only active outside ALR (periodic ALR handles capacity discovery when application-limited). This addresses a deadlock scenario where AIMD recovery stalls: after network capacity is restored, AIMD is capped at 1.5× observed throughput while the application sends at 60-70% of estimate—too high to trigger ALR (65% threshold) but too low for AIMD to make meaningful progress. The stagnation probe provides an escape mechanism by detecting prolonged lack of progress and actively testing for higher capacity. See the function documentation inprobe/control.rsfor detailed analysis of the deadlock scenario.
Gating Rules:
The controller blocks probing based on BandwidthLimitedCause state to
avoid making congestion worse:
Suppressed entirely when
DelayBasedLimitedDelayIncreased(delay increasing) orLossLimitedBwe(loss-limited and decreasing): The network is showing clear signs of congestion (increasing queuing delay or packet loss). Sending probe bursts would add more traffic to already-congested queues, exacerbating the problem and potentially triggering further rate reductions. We must wait for congestion to clear before exploring higher rates.Capped at 1.5× when
LossLimitedBweIncreasing(loss-limited but recovering): The network was recently congested but is recovering. We allow moderate probing to help the estimate grow, but cap it at 1.5× current estimate to avoid re-triggering congestion. This provides a conservative path back to higher rates.
Probes are also capped by application constraints to avoid wasteful probing:
- 2×
max_bitrate: Probes are capped at twice the application's desired sending rate to allow discovering extra capacity. This differs from WebRTC'smin(max_bitrate, 2× max_total_allocated_bitrate)wheremax_bitrateis a separate hard cap. str0m unifies these concepts—max_bitraterepresents desired sending rate (allocation), and the 2× cap allows probing for additional capacity useful for handling bursts or future demand.
The controller uses a queue to handle WebRTC's multiple-probes-at-once
pattern while maintaining str0m's one-probe-per-tick API. When a trigger
fires, it enqueues all probe configs and signals readiness via
poll_timeout(), then maybe_create_probe() drains them one at a time.
The output is a ProbeClusterConfig that flows to both the Pacer (which
executes the probe by sending packets at the target rate) and the
ProbeEstimator (which analyzes the feedback to determine how much
bandwidth was actually available).
Probe Cluster Config/State
Location: src/bwe/probe/cluster.rs
WebRTC: modules/congestion_controller/goog_cc/probe_controller.cc
(ProbeClusterConfig)
Defines what to send for a probe cluster, split into two related
structures. ProbeClusterConfig serves as the immutable blueprint
containing a unique cluster_id for TWCC tagging, the target_bitrate to
probe at, the target_duration to sustain (default 15ms), the
min_packet_count required (default 5 packets), and the min_probe_delta
(minimum time between probe bursts, default 2ms). These parameters match
WebRTC's ProbeClusterConfig structure and defaults.
ProbeClusterState handles runtime tracking during probe execution,
monitoring bytes and packets sent while calculating the next probe time
based on both the target bitrate pacing and the min_probe_delta
constraint. The min_probe_delta enforces a minimum spacing between probe
packet bursts, preventing probes from overwhelming receiver buffers with
back-to-back packets—even high-speed networks need time to process and
feedback packet arrivals. When requesting padding for a burst, the state
calculates burst_bytes = target_bitrate × min_probe_delta to ensure each
burst is large enough to achieve the target rate given the spacing
constraint.
Completion occurs when both thresholds are met: sent bytes must equal or
exceed target_bitrate × target_duration, and sent packets must equal or
exceed min_packet_count (5 packets). The state signals probe completion
to the SendSideBandwidthEstimator via the Pacer, marking the end of the
sending phase and beginning the analysis phase.
Probe Estimator
Location: src/bwe/probe/estimator.rs
WebRTC: modules/congestion_controller/goog_cc/probe_bitrate_estimator.cc
Measures how well a probe performed by analyzing TWCC feedback for the probe's packets. The estimator implements WebRTC's streaming probe estimation model where results are calculated continuously as feedback arrives rather than waiting for a fixed period.
The estimator lifecycle works as follows: probe_start() begins watching
for the cluster_id in TWCC feedback, update() accumulates received
packets while ignoring lost ones (lost probe packets indicate congestion,
making the probe result invalid) and immediately yields any new probe
results, end_probe() is called when the Pacer finishes sending and marks
the probe for cleanup, and finally handle_timeout() removes stale probe
state after the cluster history period expires.
WebRTC uses kMaxClusterHistory (1 second) as "the maximum time period
beyond which a probing burst is not expected to last." This is not a "wait
for results" period—results are calculated and emitted immediately in
update() as soon as sufficient TWCC feedback arrives. The cluster history
period ensures we continue accepting late-arriving TWCC reports for
recently-finished probes, allowing estimates to be refined as additional
feedback trickles in. After 1 second past the last received packet, the
cluster state is erased to prevent unbounded memory growth.
The calculation computes both send rate (send_size divided by send_interval
from TWCC local_send_time) and receive rate (recv_size divided by
recv_interval from TWCC remote_recv_time). It validates that recv_rate /
send_rate ≤ 2.0, rejecting probes with physically implausible timing that
would indicate measurement errors. The estimate starts with min(send_rate,
recv_rate), then applies a conservative factor if the link appears
saturated. When recv_rate is less than 0.9× send_rate
(MIN_RATIO_FOR_UNSATURATED_LINK), the link was saturated during probing
(receiver couldn't keep up with sender), so the estimate uses 0.95×
recv_rate (TARGET_UTILIZATION_FRACTION) to be conservative and avoid
pushing right to the edge of capacity. If the link remained unsaturated
(recv_rate ≥ 0.9× send_rate), the full recv_rate is used since we didn't
find the capacity limit.
Multiple validation checks gate the result: at least 4 packets must be
received (MIN_CLUSTER_SIZE) for statistical validity, at least 80% of sent
packets must arrive (MIN_RECEIVED_PROBES_RATIO) to ensure the probe
wasn't severely congested, at least 80% of sent bytes must arrive
(MIN_RECEIVED_BYTES_RATIO) to confirm byte-level delivery, send_interval
must be non-zero and not exceed 1 second (sanity checks), recv_interval must
be non-zero and not exceed 1 second, and the recv_rate / send_rate ratio
must not exceed 2.0 (physics check).
When multiple probes are updated in a single update() call, the iterator
yields results in the order they were last updated, ensuring the
DelayController receives the most recent probe result last. For ALR probes,
all successful results are forwarded to the LinkCapacityEstimator (which
takes the maximum), while the DelayController uses only the latest result.
ALR Detector
Location: src/bwe/alr_detector/detector.rs
WebRTC: modules/congestion_controller/goog_cc/alr_detector.cc
Detects when the application is sending significantly less than network
capacity, operating in what's called the Application Limited Region. This
state occurs when the application sends less than 65%
(bandwidth_usage_ratio) of the estimated capacity, indicating the
application rather than the network is the bottleneck.
The detector uses an IntervalBudget to handle bursty encoder traffic over
500ms windows. Real-world encoders produce highly bursty traffic—keyframes
are orders of magnitude larger than P-frames, scene changes cause sudden
spikes, and temporal layers create complex patterns. The IntervalBudget
implements a leaky bucket algorithm that smooths these bursts across the
window, preventing temporary spikes from falsely indicating high
utilization.
The budget operates as a balance sheet: it increases over time at 65% of the estimated bitrate (the target rate) and decreases when media packets are sent. When the application sends less than this target rate, budget accumulates (positive balance); when sending more, it depletes (negative balance, or "debt"). The budget ratio (bytes_remaining / max_bytes_in_budget) ranges from -1.0 (maximum overuse) through 0.0 (exactly at target) to 1.0 (maximum underuse).
State transitions use hysteresis thresholds to prevent rapid flapping. The
detector enters ALR when the budget ratio exceeds 0.80
(start_budget_level_ratio), indicating sustained low usage over the
500ms window. It exits ALR when the ratio drops below 0.50
(stop_budget_level_ratio), indicating increased utilization. The 30% gap
between these thresholds provides stability against oscillation—without
hysteresis, small fluctuations near the boundary would cause rapid ALR
entry/exit, disrupting probe scheduling and loss estimation.
The SendSideBandwidthEstimator calls on_media_sent() for each media
packet (excluding padding and probes), passing the packet size and
timestamp. It also calls set_estimated_bitrate() whenever the bandwidth
estimate changes, which adjusts the target rate to 65% of the new
estimate. The ALR start time flows to both ProbeControl and LossController
via alr_start_time(), providing context for probe gating decisions and
loss-based estimation during application-limited operation.
The IntervalBudget is located in src/bwe/alr_detector/budget.rs.
Link Capacity Estimator
Location: src/bwe/link_capacity_estimator.rs
WebRTC: modules/congestion_controller/goog_cc/link_capacity_estimator.cc
Tracks proven link capacity based on successful probe results obtained during Application Limited Region (ALR). When the application sends less than network capacity, probes can discover the true available bandwidth without being constrained by application sending patterns—we can send test bursts that exceed our normal traffic to find the real limit. The estimator only accepts probe results from ALR probes, as these represent genuine capacity measurements rather than application-limited observations.
The capacity estimate is stored with a timestamp and expires after 60
seconds (capacity_estimate_reset_window), since network conditions can
change over time (route changes, competing traffic, etc.). When a new ALR
probe succeeds, the estimator takes the maximum of the new result and any
existing estimate, ensuring capacity doesn't decrease from successful
measurements—only from time-based decay. This "take the max" approach is
safe because successful probe delivery proves the network can handle that
rate.
This proven capacity flows to the LossController as an upper bound during ALR operation. By capping loss-based estimates at the demonstrated capacity, it prevents the loss controller from overestimating bandwidth in scenarios where loss patterns might otherwise lead to inflated values. The link capacity integration is part of WebRTC's full LossBasedBweV2 implementation, providing more accurate estimation when operating in ALR.
Pacer
Location: src/pacer/leaky.rs (LeakyBucketPacer), src/pacer/null.rs
(NullPacer)
WebRTC: modules/pacing/pacing_controller.cc
Controls when to send packets to smooth out transmission and avoid bursts
that could overwhelm network buffers. str0m provides two implementations:
NullPacer, which performs no pacing and is used when BWE is disabled,
and LeakyBucketPacer, which implements a token bucket pacing algorithm.
The LeakyBucketPacer models pacing using two separate byte "debt" counters:
one for media and one for padding. Debt grows when packets are actually
sent (the packet size is added to the debt counters) and shrinks as time
advances (debt is reduced by bitrate × elapsed, separately for media and
padding). This debt model ensures we spread transmission over time rather
than sending in bursts. To avoid building up an unbounded burst budget
after idle periods (which would defeat the purpose of pacing), both debts
are capped to MAX_DEBT_IN_TIME (500ms) worth of bytes.
Instead of sleeping or blocking, the pacer continuously computes when it is
allowed to release the next packet and exposes that schedule via
poll_timeout() (internally tracked as next_poll_time). For normal paced
traffic, the gating signal is the media debt: the pacer computes a drain
time media_debt / adjusted_bitrate, and if this drain time exceeds the
base pacing interval PACING (40ms) the next release is scheduled in the
future; otherwise the pacer can release immediately. Streams can also be
marked as unpaced (audio by default), in which case their queued packets
bypass the pacing gate and are eligible for immediate release—audio
requires low latency and is typically low-bitrate, so it doesn't cause
burst issues. In addition, the pacer can raise adjusted_bitrate above the
configured pacing rate to drain long queues within the configured
queue_limit (default 2s), based on current queue size and the observed
average queue time—this prevents unbounded queue growth during temporary
traffic spikes.
Padding is generated in two distinct modes. In normal operation, padding is
only generated when all queues are empty and both media and padding debt
are zero (avoiding padding when real media is waiting), and it is produced
in short bursts of PADDING_BURST_INTERVAL (5ms). During an active probe
cluster, the pacer temporarily uses the probe's target_bitrate as the
effective pacing rate and uses probe-specific timing from
ProbeClusterState to decide when probe packets should be emitted; probe
padding can bypass the normal debt checks so the probe can reach its target
rate when there is insufficient media—probes need to hit their target rate
precisely to provide accurate measurements.
The pacer outputs paced packet transmission to the network and signals probe
completion events back to the SendSideBandwidthEstimator when a probe cluster
finishes sending. Queue management is handled in src/pacer/queue.rs.
Pacer Control
Location: src/pacer/control.rs
WebRTC: modules/congestion_controller/goog_cc/send_side_bandwidth_estimation.cc
(rate calculation)
Calculates pacing and padding rates based on the BWE estimate. The pacing
rate is set to 1.1× the estimate (PACING_FACTOR), providing a 10%
buffer above the estimated capacity to allow for natural transmission
variations while still smoothing out bursts. Without this buffer, normal
encoder bitrate fluctuations would constantly hit the pacing limit, causing
unnecessary queuing delays. This rate controls how the pacer smooths media
transmission timing.
The padding rate determines how much additional traffic to inject to
maintain NAT bindings and keep RTX state warm. Padding is enabled when the
current bitrate exceeds 50 kbps (MIN_PADDING_THRESHOLD)—below this, we're
barely sending anything and padding isn't needed. It's disabled during
overuse conditions to avoid worsening congestion by adding unnecessary
traffic. When active, the padding target is 50 kbps (PADDING_TARGET),
sufficient to keep middleboxes alive without adding significant overhead.
This differs from WebRTC, which bases padding decisions on the minimum simulcast layer bitrate (typically 30 kbps). str0m uses a fixed 50 kbps threshold instead, taking a simulcast-agnostic approach that works for any stream configuration.
The calculated pacing_rate and padding_rate flow to the Pacer, controlling transmission smoothing and padding generation respectively.
Differences from WebRTC
Deviations
Exponential Probing After All Probe Types:
- WebRTC: Exponential probing only chains after Initial or Exponential probes
(state-based, only when
state_ == State::kWaitingForProbingResult) - str0m: Exponential probing chains after any probe type (Initial, Exponential, IncreaseAlr, PeriodicAlr, LargeDrop, Stagnant)
- Reason: Accelerates capacity discovery in all scenarios, not just startup. If a probe succeeds (estimate increases to >70% of probe rate) regardless of probe type, the network has demonstrated capacity and we should explore higher rates immediately
- Impact: Faster convergence to true capacity after ALR probes, large-drop recovery, and stagnation probes. Reduces time spent below available capacity
- WebRTC: Exponential probing only chains after Initial or Exponential probes
(state-based, only when
Stagnant Estimate Probing:
- WebRTC: None (relies on large-drop recovery with ALR requirement, or
WebRTC-BweRapidRecoveryExperimentfield trial to remove ALR requirement) - str0m: Automatic stagnation detection and recovery probing
- Reason: Addresses AIMD deadlock where estimate stagnates after network recovery when send rate is 60-70% of estimate (above ALR threshold but below AIMD effectiveness range)
- Impact: More robust recovery from network degradation without requiring ALR entry. Probes at 2× estimate after 15s stagnation with 15s rate limit
- WebRTC: None (relies on large-drop recovery with ALR requirement, or
Padding Strategy:
- WebRTC: Based on min simulcast layer bitrate (~30 kbps)
- str0m: Fixed 50 kbps threshold and target
- Reason: Simulcast-agnostic design
- Impact: Slightly higher padding in audio-only scenarios
Bitrate Constraints:
- WebRTC: Separate
max_bitrate(hard cap) andmax_total_allocated_bitrate(allocation sum), probes capped atmin(max_bitrate, 2× max_total_allocated_bitrate) - str0m: Single
max_bitraterepresenting desired sending rate (allocation), probes capped at2× max_bitrate - Reason: str0m doesn't track per-stream bitrate allocations
- Impact: Simplified API. Probing behavior differs: str0m allows probing up to 2× desired rate without a separate hard cap, while WebRTC constrains by the minimum of the two values
- WebRTC: Separate
Allocation Probing Trigger:
- WebRTC: Edge-triggered on
OnMaxTotalAllocatedBitrate()calls during ALR (probes only when allocation changes while in ALR) - str0m: Level-triggered on
estimate < max_bitratecondition (probes immediately on ALR entry and every 5 seconds while in ALR with headroom) - Reason: Better handles network recovery scenarios where application's desired bitrate remains constant but network capacity improves
- Impact: More aggressive capacity rediscovery during ALR. Ensures consistent application demand (e.g., "I want 5 Mbps") triggers probing whenever the network might support it, not just when the demand value changes
- WebRTC: Edge-triggered on
Shared with WebRTC
- Core delay-based detection algorithm (trendline estimator with AIMD rate control)
- Probe controller state machine and decision logic with WebRTC default constants
- Probe cluster configuration (target duration, min packet count, min probe delta)
- Initial exponential probing (3×, 6× with further probing at 2×)
- Large-drop recovery probing (85% of pre-drop rate)
- Bandwidth-limited-cause gating for probing
- Arrival group formation rules
- Probe bitrate estimation (send/recv rate calculation and ratio validation)
- Acked bitrate estimation (Bayesian smoothing)
- Loss-based estimation with ALR integration and link capacity tracking
- ALR detection for probe scheduling and loss controller bounds
Field Trials to Monitor
These WebRTC features remain behind field trials (disabled by default). str0m does not implement them until they become permanently enabled in WebRTC.
Periodic ALR Probing
- API:
StreamsConfig.requests_alr_probing = true - Behavior: Sends probes every 5 seconds at 2× current estimate when in ALR to rediscover capacity
- WebRTC constants:
alr_probing_interval = 5s,alr_probe_scale = 2.0 - Status: Optional feature, must be explicitly enabled by application
- str0m note: This feature is effectively implemented in str0m (always enabled when BWE is active). We monitor this field trial to track whether WebRTC takes the implementation in a different direction.
Repeated Initial Probing
- API:
StreamsConfig.enable_repeated_initial_probing = true - Behavior: Repeats initial probing every 1 second for the first 5 seconds after network becomes available
- WebRTC constants:
repeated_initial_probing_time_period = 5s,initial_probe_duration = 100ms,initial_min_probe_delta = 20ms - Purpose: Rapidly discover capacity during startup, ignores
max_total_allocated_bitrate - Status: Optional feature, disabled when allocation is set
Rapid Recovery Experiment
- Field trial:
WebRTC-BweRapidRecoveryExperiment - Behavior: Enables large-drop recovery probing outside of ALR (in addition to ALR-based recovery)
- Status: Experimental, requires field trial opt-in
Abort Further Probing
- Field trial:
WebRTC-Bwe-ProbingConfiguration/abort_further:true/ - Behavior: Aborts further exponential probing if estimate exceeds
max_bitrateor2 × max_total_allocated_bitrate - Default:
false - Status: Available via field trial
Skip Probing When Estimate Is High
- Field trial:
WebRTC-Bwe-ProbingConfiguration/skip_if_estimate_larger_than_fraction_of_max:<fraction>/ - Behavior: Skips probing if
estimated_bitrate > max_probe_rate × fraction - Default:
0.0(disabled) - Status: Available via field trial
Network State Estimate Probing
- Field trial:
WebRTC-Bwe-ProbingConfiguration/network_state_interval:<duration>/ - Behavior: Probes based on external network state estimates
- Default:
TimeDelta::PlusInfinity()(disabled) - Status: Requires external network state provider, not enabled in defaults