# 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: 1. **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. 2. **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. 3. **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 BWE Data Flow **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:** 1. Groups packets into arrival groups (via `ArrivalGroupAccumulator`) 2. Calculates inter-group delay variations 3. Feeds delay variations to trendline estimator 4. 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 5. Uses rate control (AIMD) to adjust bandwidth estimate at regular intervals (`UPDATE_INTERVAL` = 25ms) 6. 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:** 1. **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. 2. **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. 3. **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. 4. **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. 5. **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 in `probe/control.rs` for 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) or `LossLimitedBwe` (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's `min(max_bitrate, 2× max_total_allocated_bitrate)` where `max_bitrate` is a separate hard cap. str0m unifies these concepts—`max_bitrate` represents 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 1. **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 2. **Stagnant Estimate Probing:** - **WebRTC**: None (relies on large-drop recovery with ALR requirement, or `WebRTC-BweRapidRecoveryExperiment` field 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 3. **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 4. **Bitrate Constraints:** - **WebRTC**: Separate `max_bitrate` (hard cap) and `max_total_allocated_bitrate` (allocation sum), probes capped at `min(max_bitrate, 2× max_total_allocated_bitrate)` - **str0m**: Single `max_bitrate` representing desired sending rate (allocation), probes capped at `2× 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 5. **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_bitrate` condition (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 ### 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_bitrate` or `2 × 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:/` - **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:/` - **Behavior**: Probes based on external network state estimates - **Default**: `TimeDelta::PlusInfinity()` (disabled) - **Status**: Requires external network state provider, not enabled in defaults