QUIC’s congestion control is fundamentally different because it lives in userspace, not the kernel, allowing for rapid iteration and a diversity of algorithms beyond TCP’s traditional Reno/Cubic.
Let’s see how this plays out with a simulated QUIC connection. Imagine a server sending data to a client over a somewhat lossy network.
# This is conceptual Python, not runnable code.
# It illustrates the state changes in a QUIC congestion controller.
class QUICCongestionController:
def __init__(self, algorithm="BBR"):
self.algorithm = algorithm
self.bandwidth = 0 # Estimated bandwidth in bytes/sec
self.min_rtt = float('inf') # Minimum RTT in microseconds
self.packets_in_flight = 0
self.ssthresh = float('inf') # Slow start threshold
self.cwnd = 10 # Congestion window in packets
def on_packet_sent(self, packet_size):
self.packets_in_flight += 1
def on_packet_received(self, packet_size, rtt):
self.packets_in_flight -= 1
self.min_rtt = min(self.min_rtt, rtt)
if self.cwnd < self.ssthresh: # Slow Start
self.cwnd += 1
else: # Congestion Avoidance
# Add a fraction of a packet per RTT to avoid large jumps
self.cwnd += max(1, packet_size / self.cwnd)
# Update bandwidth estimation based on RTT and bytes acknowledged
# (Simplified: actual algorithms are more complex)
self.bandwidth = packet_size * (1_000_000 / rtt) # Bytes per second
def on_packet_lost(self, packet_size):
self.packets_in_flight -= 1
# Significant reduction in CWND and ssthresh
self.ssthresh = self.cwnd / 2
self.cwnd = self.ssthresh
# Reset min_rtt to potentially re-probe for bandwidth
self.min_rtt = float('inf')
# --- Simulation ---
controller = QUICCongestionController()
packet_size = 1400 # bytes
base_rtt = 50_000 # microseconds (50ms)
# Initial slow start
for _ in range(15):
controller.on_packet_sent(packet_size)
# Simulate successful ACK
controller.on_packet_received(packet_size, base_rtt)
print(f"CWND: {controller.cwnd}, Packets in Flight: {controller.packets_in_flight}, Min RTT: {controller.min_rtt}, BW: {controller.bandwidth}")
# Entering congestion avoidance
for _ in range(10):
controller.on_packet_sent(packet_size)
controller.on_packet_received(packet_size, base_rtt + 5000) # Slight RTT increase
print(f"CWND: {controller.cwnd}, Packets in Flight: {controller.packets_in_flight}, Min RTT: {controller.min_rtt}, BW: {controller.bandwidth}")
# Simulate packet loss
controller.on_packet_sent(packet_size)
controller.on_packet_lost(packet_size)
print(f"CWND: {controller.cwnd}, Packets in Flight: {controller.packets_in_flight}, Min RTT: {controller.min_rtt}, BW: {controller.bandwidth}")
# Recovery phase
for _ in range(5):
controller.on_packet_sent(packet_size)
controller.on_packet_received(packet_size, base_rtt)
print(f"CWND: {controller.cwnd}, Packets in Flight: {controller.packets_in_flight}, Min RTT: {controller.min_rtt}, BW: {controller.bandwidth}")
This simulation shows the core loop: sending packets, acknowledging them, and reacting to loss. The cwnd (congestion window) dictates how many packets can be in flight. When ACKs arrive, cwnd grows. When loss occurs, cwnd and ssthresh drop sharply. The key difference from TCP is that the entire logic for on_packet_received and on_packet_lost is implemented in the QUIC library (like quiche, lsquic, or mvfst) on the client and server, not buried in the operating system kernel.
QUIC’s congestion control solves the problem of network congestion while aiming for higher throughput and lower latency than traditional TCP, especially over lossy or high-latency links. It achieves this by:
- User-space implementation: Allows for faster development and deployment of new algorithms.
- Loss detection: Uses packet number-based loss detection (rather than TCP’s triple-ACK timeout), which is more robust and faster.
- RTT measurement: Tracks RTT per packet, enabling more granular adjustments.
- Pluggable algorithms: Supports various algorithms, with Google’s BBR (Bottleneck Bandwidth and Round-trip propagation time) being a popular choice.
The primary levers you control are the choice of algorithm and its specific tuning parameters. The most common algorithms you’ll encounter are:
- Reno/Cubic: Traditional TCP algorithms, often included for compatibility. Cubic is generally more aggressive on high-bandwidth, high-latency links.
- BBR (v1 and v2): A newer algorithm that aims to model the network path’s bottleneck bandwidth and RTT. BBR v1 can sometimes "fill the pipe" aggressively, leading to bufferbloat. BBR v2 attempts to be more conservative and work better with other BBR flows.
- PCC (Performance-Oriented Congestion Control): A framework for developing and testing custom congestion control algorithms.
Tuning involves selecting an algorithm and then adjusting its parameters. For BBR, this might mean influencing its minimum RTT sampling or bandwidth estimation. For example, on systems where RTT measurements might be skewed by background traffic, you might adjust parameters related to how BBR filters RTT samples.
Here’s how you might configure BBR in lsquic (a popular QUIC library):
# Example: Setting BBR parameters via command-line flags for an lsquic server
# These are illustrative; actual flags may vary by lsquic version.
./my_lsquic_server --congestion-control bbr \
--bbr-min-rtt-filter-gain 0.1 \
--bbr-bandwidth-filter-gain 0.01
In quiche (another library), it’s often built into the CongestionControl trait, and you’d select an implementation.
// Conceptual Rust code for quiche
use quiche::congestion_control::{
Algorithm, BBR, Cubic, CongestionControlConf
};
let mut conf = CongestionControlConf::default();
// conf.set_initial_rtt(Duration::from_millis(100)); // Example tuning
// conf.set_max_ack_delay(Duration::from_millis(25)); // Example tuning
let cc_algorithm: Box<dyn Algorithm> = match algo_name {
"bbr" => Box::new(BBR::new(conf)),
"cubic" => Box::new(Cubic::new(conf)),
_ => panic!("Unknown algorithm"),
};
The bbr-min-rtt-filter-gain controls how aggressively BBR updates its minimum RTT estimate. A lower gain means it’s slower to react to increases in RTT, potentially sticking to a stale lower RTT and over-utilizing the pipe. The bbr-bandwidth-filter-gain similarly affects how quickly the bandwidth estimate is updated. Lower values make the estimate more stable but slower to adapt to changes.
A common misunderstanding is that BBR’s goal is to eliminate bufferbloat. While it aims to reduce it by not constantly filling buffers, its primary mechanism is to probe for bandwidth and RTT. If the network path has large buffers, BBR might indeed fill them as it tries to find the bottleneck, leading to increased latency. The fix for this often lies in network infrastructure (router buffer tuning) or using BBRv2, which has mechanisms to avoid over-utilizing buffers when it detects them.
The next rabbit hole you’ll likely fall down is how QUIC handles packet loss detection and recovery, particularly the distinction between spurious retransmissions and actual packet loss.