TCP congestion control in practice
Every few months I end up explaining the same thing: BBR is not "faster TCP". It is a different answer to the question "how fast should I send?" and it gives a better answer only under specific conditions. Get those conditions wrong and it can be worse than the default.
The two questions
Congestion control is really two decisions stacked on top of each other: how much data can be in flight (the congestion window), and how do you know when you have sent too much. Loss-based algorithms answer the second question with packet loss. Model-based algorithms answer it with measurements of bandwidth and round-trip time.
CUBIC, the Linux default, is loss-based. It grows the window until something drops, then backs off multiplicatively. On a clean link this is close to optimal. On a link where loss happens for reasons other than congestion — a dirty microwave backhaul, a policer that drops bursts, a satellite path — it backs off for no reason and never recovers its fair share.
BBR builds a model instead. It measures delivery rate and minimum RTT, then paces at what it believes is the bottleneck bandwidth. Loss is treated as a signal, not a verdict.
What the difference looks like
Same pair of hosts, same 200 ms path, only the loss rate changes. Numbers below are from iperf3 over sixty seconds, single stream, warm cache on both ends:
| Path loss | CUBIC | BBR |
|---|---|---|
| 0.00% | 412 Mbps | 398 Mbps |
| 0.10% | 88 Mbps | 371 Mbps |
| 1.00% | 11 Mbps | 302 Mbps |
| 5.00% | 2.1 Mbps | 147 Mbps |
The first row is the part people skip. On a clean link CUBIC is not slower — it is marginally faster, because BBR spends a small amount of capacity probing for more bandwidth it will never find. If your traffic never crosses a lossy path, switching buys you nothing and costs you a little.
The cost nobody puts in the table
BBR is aggressive. Two BBR flows sharing a bottleneck with a CUBIC flow will take most of the capacity, because BBR's model does not include a fairness term. On a link you control end to end, that is fine. On a shared link, you are the reason someone else's transfer is slow, and they have no way to know why.
BBR is a local optimisation. It has no notion of being polite, and that is a design choice rather than a bug.
I still enable it on boxes where the path is bad and the traffic is mine. I do not enable it on anything that shares a congested uplink with strangers.
Enabling it
Two sysctls, plus a module load that is easy to forget:
modprobe tcp_bbr
cat >> /etc/sysctl.d/90-bbr.conf <<'EOF'
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF
sysctl --system
sysctl net.ipv4.tcp_congestion_control
The fq qdisc matters. BBR paces packets, and pacing needs a qdisc that supports it. Setting the congestion control without the qdisc gives you BBR's window behaviour with none of its pacing, which is the worst of both worlds and is not obvious from the output of sysctl.
Measuring honestly
A single iperf3 run proves nothing. I use three checks before believing a number:
- Run long enough to leave slow start — thirty seconds minimum, sixty is better.
- Check
ss -tinduring the transfer. Ifcwndis still climbing when the run ends, you measured the ramp, not the steady state. - Repeat at a different time of day. A path that is clean at 04:00 and lossy at 20:00 will make you draw the wrong conclusion.
If the two algorithms disagree by more than a factor of two, something interesting is happening on the path and it is worth finding out what. If they agree within ten percent, the path is clean and you should spend your afternoon on something else.