
In the TCP article, we covered Reno/CUBIC congestion control in detail: slow start grows exponentially, congestion avoidance grows linearly, and a packet loss halves the window. This mechanism has dominated the internet for nearly 30 years and remains the default algorithm in the Linux kernel.
But here's a question worth asking: why wait for packet loss before slowing down?
A packet loss means some router's queue along the path is already full — the packet has been ruthlessly discarded. But long before that, the queue had already accumulated tens or even hundreds of milliseconds of data, and latency had already skyrocketed. Reacting only after a loss is like discovering a wall by crashing into it.
This is the core problem BBR (Bottleneck Bandwidth and Round-trip propagation time) sets out to solve. In 2016, Google published BBR, which doesn't use packet loss to infer congestion — instead, it directly measures the path — finding the bottleneck bandwidth and minimum RTT, then sending at exactly that rate, neither flooding queues nor wasting bandwidth.
Imagine you're downloading a 1GB file. Your bandwidth is 100 Mbps, RTT is 20ms. In theory, the download should be fast. But you notice erratic speed — full speed one moment, halved the next — accompanied by noticeable latency jitter.
Wireshark reveals a TCP sequence number timeline that looks like a roller coaster:
Send rate
│ ╱╲ ╱╲
│ ╱ ╲ ╱ ╲ ← classic CUBIC sawtooth
│╱ ╲══╱ ╲══
└──────────────────→ timeCUBIC constantly "probes" — increasing the window until a loss occurs, then slashing it and creeping back up. The problem: by the time a loss occurs, the router's buffer is already full, and RTT has spiked from 20ms to 200ms. You've not only wasted bandwidth (retransmissions) but also added latency (queue buildup).
Google engineers observed this at scale on YouTube. Their inter-datacenter links are high-speed fiber with single-digit-millisecond RTTs, and traditional algorithms perform terribly on such networks. So they asked the key question:
Can we fully utilize bandwidth without building up queues?
The answer is BBR.
Traditional congestion control (Reno, CUBIC) relies on an implicit assumption: packet loss = congestion. In the early internet, router buffers were small; a full queue causing loss was a fairly accurate signal. But today:
This phenomenon is called bufferbloat:
Router buffer
Capacity: 200 packets
Normal: Bufferbloat:
┌────┐ ┌────────────────────┐
│ 5 │ ← short queue │■■■■■■■■■■■■■■■■■■■■│ ← queue full
└────┘ RTT = 20ms │■■■■■■■■■■■■■■■■■■■■│
└────────────────────┘
RTT = 220ms (+200ms queueing delay)
↓
Loss! ← only NOW does the sender reactBBR's creator Neal Cardwell put it succinctly: packet loss is not a congestion signal — it's a late congestion signal. It tells you the queue was full long ago.
BBR's solution: don't guess from loss. Measure the path's physical limits directly.
A network path has two physical limits, independent of TCP's sending strategy, determined purely by link hardware:
Bottleneck Bandwidth (BtlBw): The bandwidth of the narrowest link along the path. On a 100 Mbps link, no matter how fast the sender transmits, the router can only forward at 100 Mbps.
Round-trip propagation time (RTprop): The minimum time for data to travel across the physical medium and back — the sum of all propagation delays, excluding any queueing time. The speed of light in fiber is fixed; this value is the physical lower bound of the path.
BBR's core idea is elegantly simple:
At all times, maintain sending rate = BtlBw, and inflight data = BtlBw × RTprop.
This inflight data quantity is what BBR calls the BDP (Bandwidth-Delay Product):
BDP = bottleneck bandwidth × minimum RTT
Example: 100 Mbps bandwidth, 20ms RTT
BDP = 100 × 10^6 bps × 0.020s = 2,000,000 bits ≈ 250 KBWhen inflight data exactly equals BDP, the pipe is full but nothing is queued — data fills the pipe perfectly, wasting no bandwidth and building up no buffer.
BDP perfectly fills the pipe:
Sender ══════════════════════════════→ Receiver
←══════════════════════════════
|←──────── RTT ──────────→|
BDP too small (pipe underutilized):
Sender ═══╪══════════════════════════→ Receiver
←═══════════════════════════
BDP too large (queue buildup):
Sender ═══════════════════════════════════╪→ Receiver
←═══════════════════════════════════
↑ queueing delayBBR must continuously estimate BtlBw and RTprop, and both can change over time (route changes, link fluctuations).
Measuring BtlBw (bottleneck bandwidth):
BBR computes an instantaneous delivery rate on every ACK:
delivery_rate = data ACKed this time / time since last ACKThen it takes the maximum over a sliding window (typically 6-10 RTTs). Why the maximum? Because the network is sometimes idle (nobody sending), so a low measured rate doesn't mean the link is slow. The maximum is the true estimate of bottleneck capacity.
Measuring RTprop (minimum RTT):
BBR tracks the minimum RTT observed over a sliding window (typically 10 seconds). Why the minimum? Any RTT larger than the minimum includes queueing delay. The minimum RTT = pure propagation delay, the physical limit of the path.
RTT measurements:
├─ 20ms ← no queueing (true RTprop)
├─ 21ms
├─ 20ms ← minimum!
├─ 85ms ← queueing
├─ 22ms
└─ 20ms
BBR records RTprop = 20ms (takes the minimum)These two measurement strategies reveal an elegant truth: bandwidth takes the maximum, delay takes the minimum. The two describe "how thick" and "how long" the pipe is, respectively.
BBR maintains two key control variables:
BBR v1 has four phases forming a cycle:
┌──────────┐
│ Startup │ exponential growth, discovering BtlBw
└────┬─────┘
↓
┌──────────┐
│ Drain │ empty the queue built up during Startup
└────┬─────┘
↓
┌─────────────────────┐
│ ProbeBW │ steady state: periodic bandwidth probing
└──────────┬──────────┘
↓ ↑
┌────────┐ │
│ProbeRTT│ │ periodically probe for a lower RTprop
└────────┘ │
└──────┘Similar to TCP slow start, rapidly probing for the bottleneck bandwidth. But unlike slow start's "exponentially increase cwnd until loss," BBR watches whether the delivery rate is still increasing:
During Startup:
pacing_rate = pacing_rate × 2/ln2 (doubles every RTT)
Exit condition:
delivery_rate increases by less than 25% for 3 consecutive RTTs
→ bottleneck bandwidth has been reached; further sending only builds queuesNote: BBR's Startup does not exit on packet loss — it exits when "bandwidth stops growing." This is far gentler than traditional slow start — it won't blow out the queue before stopping.
During Startup, BBR intentionally sends slightly above bottleneck bandwidth, leaving some backlog in router queues. The Drain phase empties this backlog, bringing RTT back down to RTprop:
pacing_rate = estimated BtlBw × (1 / 2/ln2)
≈ BtlBw × 0.65 ← deliberately below bottleneck to drain queues
Exit condition:
inflight data ≤ BDP
→ queues are empty, ready for steady stateThis is where BBR spends ~98% of its time. It uses an 8-RTT cycle, oscillating slightly around the estimated bottleneck bandwidth:
One ProbeBW cycle (8 RTTs):
pacing_rate as a multiple of BtlBw:
RTT 1: ×1.25 ← probe up: is more bandwidth available?
RTT 2: ×0.75 ← drain down: clear any accumulated queue
RTT 3: ×1.00
RTT 4: ×1.00
RTT 5: ×1.00
RTT 6: ×1.00
RTT 7: ×1.00
RTT 8: ×1.00Every 8 RTTs, a "probe" (×1.25) tests whether more bandwidth is available. If ACKs return at a genuinely higher rate, the bottleneck has truly expanded (e.g., a route changed to a better path), and the BtlBw estimate updates. If not, the following ×0.75 drains any queue buildup.
This "periodic small probe" design is elegant: most of the time, stay at exactly BDP; occasionally nudge the boundary; discover new bandwidth without causing loss.
BBR also has a long-cycle probe: if the RTprop estimate hasn't been updated for 10 seconds, it deliberately drops inflight to a very low level (4 packets) for at least 200ms, letting all queues drain completely to obtain a true, fresh minimum RTT:
Trigger condition:
RTprop estimate unchanged for 10 seconds (no empty-queue moment observed)
Behavior:
cwnd drops to 4 packets, held for ≥ 200ms
→ zero queueing anywhere; measured RTT is pure physical propagation delay
Exit:
After 200ms, resume normal pacing_rateThis ensures BBR's understanding of path latency doesn't "drift" — if the link changed or a fiber was cut, RTprop will be rediscovered within at most 10 seconds.
CUBIC (loss-based): BBR (model-based):
Throughput Throughput
│ ╱╲ ╱╲ │ ─────────────────
│ ╱ ╲ ╱ ╲ ← sawtooth │ ← flat!
│╱ ╲╱ ╲ │
└──────────────→ time └──────────────→ time
RTT RTT
│ ╱╲ │ ─────────
│ ╱ ╲ ← periodic spikes │ ← stable!
│ ══╱ ╲ │
└──────────────→ time └──────────────→ time
Packet Loss Packet Loss
│ │ │ │ │ │
│ │ │ │ │ ← frequent loss │ ← almost no loss
└──────────────→ time └──────────────→ time| Dimension | CUBIC | BBR |
|---|---|---|
| Core signal | Packet loss | Bandwidth + RTT measurement |
| Judgment method | Reactive (wait for loss) | Proactive (model physical limits) |
| Throughput curve | Sawtooth oscillation | Flat, near bottleneck bandwidth |
| Queue behavior | Periodically fills buffers | Maintains minimum levels |
| Latency | Periodic spikes | Stable near RTprop |
| Loss rate | Relies on loss as "brake" | Produces almost no loss |
| High-BDP / fiber links | Slow, takes long to fill window | Extremely fast, Startup probes exponentially |
| Mild loss (WiFi) | False positive — treats as congestion | Unaffected (unless bandwidth actually changed) |
BBR v1 issues:
BBR v2 (2019):
Introduced a lightweight loss response — while keeping the core model-based philosophy — that reduces sending rate when loss exceeds a threshold (e.g., 1%). Also improved ProbeBW convergence speed and fairness.
BBR v3 (2023):
Further improved fairness when coexisting with CUBIC, and enhanced performance on Wi-Fi and cellular networks. Currently being upstreamed into the Linux kernel mainline.
BBR is already widely deployed across Google's internal network and user-facing services (YouTube, Google.com). It is also the default congestion control algorithm for QUIC/HTTP3.
Check your system's congestion control algorithm
# View available algorithms
sysctl net.ipv4.tcp_available_congestion_control
# View currently active algorithm
sysctl net.ipv4.tcp_congestion_controlMost Linux distributions default to cubic.
Enable BBR (Linux, requires root)
# Load the BBR kernel module
sudo modprobe tcp_bbr
# Set as the default congestion control algorithm
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
# Verify
sysctl net.ipv4.tcp_congestion_control
# Output: net.ipv4.tcp_congestion_control = bbrAfter this, all new TCP connections will use BBR. No reboot required.
Compare BBR and CUBIC throughput
Use iperf3 for a side-by-side test (requires two machines, one as server, one as client):
# Server (run on both machines)
iperf3 -s
# Client — CUBIC
sudo sysctl -w net.ipv4.tcp_congestion_control=cubic
iperf3 -c <server_ip> -t 30
# Client — BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
iperf3 -c <server_ip> -t 30Compare throughput, especially under high-latency, lossy conditions.
Simulate high latency + loss with tc, observe BBR vs. CUBIC
# Add 50ms latency + 0.5% loss on eth0
sudo tc qdisc add dev eth0 root netem delay 50ms loss 0.5%
# Test CUBIC and BBR throughput with iperf3
# ...
# Clean up
sudo tc qdisc del dev eth0 rootWith packet loss present, CUBIC's throughput may drop dramatically (it treats loss as congestion), while BBR is largely unaffected — BBR measures that bandwidth hasn't changed, so it continues at the original rate.
Observe BBR's internal state with ss
# After establishing a long-lived connection
ss -t -i 'sport = :443 or dport = :443'The output includes congestion control internal variables. BBR connections show the bbr tag and parameters like pacing_rate and delivery_rate. Compare with CUBIC connections (which show cwnd, ssthresh) to intuitively grasp the difference.
net/ipv4/tcp_bbr.c — well-commented and an excellent textbook example of algorithm implementationWith BBR, we've covered the modern evolution of transport-layer congestion control. Up to this point, all data — including TCP sequence numbers, ACKs, and application payload — has been transmitted in plaintext over the network. Any intermediate router can read your git push contents. Any coffee shop Wi-Fi can sniff your cookies.
The next article enters the security layer: how do we establish a secure channel over an insecure public network? The answer is TLS — the collaboration between symmetric and asymmetric encryption, certificates and PKI, handshake optimization from TLS 1.2 to 1.3, and the full story behind that "little green padlock" in HTTPS.