Comparing congestion control algorithms by download speed alone is how you end up with a tuning change that helps one flow shape and hurts another. The kernel exposes a far better measurement: tcp_info fields readable through ss -i, including the delivery rate the sender itself estimates. Compare BBR and CUBIC using those fields and the reason for the difference becomes visible rather than anecdotal.
Enable BBR correctly and verify it
BBR needs both the congestion control module and fair queueing on the egress path. Setting the sysctl without an fq qdisc leaves tcp_congestion_control reported as bbr while pacing falls back to the default behaviour, which is the most common cause of “BBR did nothing” reports.
modprobe tcp_bbr
sysctl net.ipv4.tcp_congestion_control=bbr
sysctl net.core.default_qdisc=fq
sysctl -w net.ipv4.tcp_available_congestion_control
# net.ipv4.tcp_available_congestion_control = reno cubic bbr
sysctl -w net.ipv4.tcp_congestion_control
ip route show default
# confirm the qdisc actually attached
ip -s link show dev eth0 | head -5
tc qdisc show dev eth0
Persist with /etc/sysctl.d/99-bbr.conf and load the module at boot via /etc/modules-load.d/bbr.conf. Verify after a reboot rather than assuming.
Read the right tcp_info fields
Run a transfer and inspect the socket while it is live. ss -tin prints a set of fields that are the actual evidence:
ss -tin state established '( dport = :443 or sport = :443 )' | head -20
# fields to compare:
# rtt:X/Y smoothed rtt / rtt variance, microseconds
# cwnd:N congestion window in segments
# send Zbps application-limited send rate
# bw = X BBR's delivery-rate estimate (only present under bbr)
# retrans:X/Y retransmitted / total segments
# fack, lost, dsack loss accounting
# pacing_rate the pacer's current target (bbr only)
Under CUBIC, bw and pacing_rate are absent, so you infer throughput from cwnd divided by rtt. Under BBR, bw is the sender’s own estimate of the bottleneck delivery rate – a much more direct number, and one that stays informative when the connection is application-limited and the cwnd-derived estimate collapses to nonsense.
| Condition | CUBIC behaviour | BBR behaviour | Diagnostic |
|---|---|---|---|
| 1% random loss | halves cwnd, throughput drop | largely ignores non-congestive loss | retrans count vs cwnd curve |
| 128 KB buffer at bottleneck | loss-driven sawtooth | probes for the buffer floor | compare bw to paced rate |
| High RTT (150 ms+) | slow convergence | converges in ~1 RTT | rtt field plus bw ramp |
| Shallow buffer, no loss | cwnd grows until loss | stabilizes below queue limit | tc -s qdisc backlog |
Measure both without network noise
Interleave the two algorithms against the same endpoint within the same minute to cancel out external load:
for cc in cubic bbr; do
sysctl -qw net.ipv4.tcp_congestion_control=$cc
echo "== $cc =="
curl -o /dev/null -s -w 'connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total} bytes=%{size_download}\n' \
--limit-rate 0 https://mirror.example.net/100mb.bin
sleep 5
done
# per-connection retransmit view during the run
nstat -az TcpRetransSegs TcpExtTCPLostRetransmit TcpExtTCPFastRetrans
# queue backlog at the egress
tc -s qdisc show dev eth0
On a VPS, the uplink is usually shared and the bottleneck is frequently the hypervisor’s vNIC or the upstream switch port, not the path. If bw plateaus at a value far below your provisioned port rate and tc -s qdisc shows a persistent backlog with zero drops, you are queue-limited by the host, and no congestion control change will help. Detecting that early saves a lot of tuning.
Where BBR is the wrong answer
BBR v1 is aggressive about probing for bandwidth and can unfairly starve CUBIC flows sharing a bottleneck, which matters on a VPS whose uplink is policed by the provider. It also does not respond to loss the way operators expect, so a genuinely congested path can show high bw estimates while real goodput – the bytes the peer confirms – stays flat. Cross-check with application-level counters, not just ss.
- Keep BBR for long-haul, high-RTT, moderately lossy paths (intercontinental replication, file sync to remote regions).
- Keep CUBIC for low-RTT paths where you share the link with other tenants and fairness matters.
- Use
net.core.default_qdisc=fqwith BBR andfq_codelwith CUBIC; a mismatched qdisc hides the effect you are measuring. - Check
nstatretransmit counters between runs, not only wall-clock transfer time.
If the delivery-rate estimate saturates at a rate well under your port speed during every test, the constraint is the host’s egress policing rather than the kernel. That is a provisioning question: our VPS network specifications and port-rate details list the sustained egress each tier is policed to, which is the number to compare your measured bw against.
Watch the pacer, not the peak
BBR’s advantage is stability under shallow buffers, and pacing_rate is where it shows. Sample it repeatedly during a transfer and look at the variance rather than the maximum. A pacer that holds a tight band around a stable rate is doing its job; one that swings by a factor of three between samples indicates the delivery-rate estimate is being perturbed by loss or by another flow on the same bottleneck.
# sample pacing_rate and delivery rate 10x during a transfer
for i in $(seq 10); do
ss -tin state established '( dport = :443 )' | grep -o 'bw=[^ ]*\|pacing_rate [^ ]*'
sleep 1
done | sort | uniq -c | sort -rn
# correlate with host-level egress contention
cat /proc/net/dev | awk '/eth0/{print "tx_drop=" $4, "tx_fifo=" $3}'
mpstat -P ALL 1 3
If the variance is large and tx_fifo grows while tx_drop stays flat, the queue on the host side is absorbing the bursts and adding latency without losing packets – a bufferbloat symptom, best addressed with fq_codel and a sane txqueuelen rather than with a congestion control swap.
Sizing the test to the instance
Congestion control comparison on a 1-vCPU instance has a hard ceiling: a single core may not generate enough packets to saturate a multi-gigabit uplink, in which case the sender is the bottleneck and both algorithms measure the same wall. Check mpstat for the softirq column during the transfer. If softirq approaches one full core, move to a larger instance or pin the receiver across queues before drawing conclusions about the algorithm. The vCPU and port-rate figures per tier are listed on our VPS plans and network specifications page.


Leave a Reply
You must be logged in to post a comment.