LinuxXLII · Network PerformanceLong fat networks
The bandwidth-delay product - why a fast link runs slow
What you'll learn
- Compute the bandwidth-delay product for a path and compare it against the achievable window
- Read cwnd, rtt, delivery_rate and bytes_retrans from ss -ti and diagnose a single flow
- Explain why a single TCP flow cannot fill a long, lossy link, using the Mathis bound
- Decide between cubic and bbr from evidence, and know what bbr does not fix
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11
A 10 Gb/s link between two continents delivers 8 Mbit/s to a
single scp. Both NICs report a clean link, a ping test shows no
loss, and the switches are idle. This is the most common
“the network is slow” report that is not a network fault, and it
is entirely predictable from two bounds - one set by the window
size, one set by the loss rate. Neither of them involves the link
rate.
The bandwidth-delay product
TCP can have at most one window of unacknowledged data in flight. To keep a pipe full, the window must be at least as large as the data that fits in the pipe:
BDP (bytes) = bandwidth (bits/s) / 8 x RTT (seconds)
Measure the RTT first:
ping -c 20 198.51.100.24 | tail -2
20 packets transmitted, 20 received, 0% packet loss, time 19031ms
rtt min/avg/max/mdev = 148.221/149.882/152.004/0.911 ms
Then compute, for a 10 Gb/s path at 150 ms:
awk 'BEGIN {
bw_bps = 10e9
rtt_s = 0.150
bdp = bw_bps / 8 * rtt_s
printf "BDP = %.1f MB\n", bdp / 1048576
}'
BDP = 178.8 MB
To fill that path with one flow, 179 MB must be in flight. Now compare against what the kernel will allow:
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.core.rmem_max net.core.wmem_max
net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304
net.core.rmem_max = 212992
net.core.wmem_max = 212992
The three values in tcp_rmem are minimum, default and
maximum. Receive autotuning will grow the buffer up to
6 MB - which is 3% of the 179 MB needed. These defaults vary by
distribution and by kernel version, so read them on the host
rather than assuming the numbers above; some distributions ship a
32 MB ceiling, which changes the arithmetic but not the shape of
the problem.
The ceiling on throughput follows directly from whatever the maximum turns out to be:
awk 'BEGIN {
win_bytes = 6291456
rtt_s = 0.150
printf "ceiling = %.1f Mbit/s\n", win_bytes * 8 / rtt_s / 1e6
}'
ceiling = 335.5 Mbit/s
335 Mbit/s on a 10 Gb/s link, with no loss and no congestion. The link is not slow; the window is small.
Window scaling must also be on, or the window cannot exceed 64 KB regardless of the buffers:
sysctl net.ipv4.tcp_window_scaling
net.ipv4.tcp_window_scaling = 1
It has been the default for decades. It is worth checking anyway, because a middlebox that strips the window-scale TCP option produces exactly this symptom on a path where both endpoints are configured correctly.
Per-flow evidence with ss -ti
Arithmetic predicts a ceiling. ss -ti shows what a live flow
actually achieved:
ss -ti state established '( dport = :443 )' | head -20
ESTAB 0 1179648 192.0.2.15:51234 198.51.100.24:443
cubic wscale:7,7 rto:352 rtt:149.882/0.911 mss:1448 pmtu:1500
rcvmss:536 advmss:1448 cwnd:1443 ssthresh:721 bytes_sent:184320000
bytes_retrans:1024 bytes_acked:184318976 segs_out:127293
data_segs_out:127280 send 111.5Mbps lastsnd:4 lastrcv:1204
pacing_rate 133.8Mbps delivery_rate 108.2Mbps busy:1651ms
retrans:0/12 rcv_space:14480 rcv_ssthresh:64088 minrtt:148.221
The fields that matter:
| Field | Meaning |
|---|---|
cubic | The congestion control algorithm in use for this flow |
rtt:149.882/0.911 | Smoothed RTT and its variance, in ms |
minrtt | The lowest RTT ever seen - the path’s true propagation delay |
cwnd:1443 | Congestion window, in segments. Multiply by mss for bytes |
ssthresh | Slow-start threshold; a value below cwnd means loss has been seen |
bytes_retrans | Bytes retransmitted on this flow |
delivery_rate | The rate actually achieved, measured by the kernel |
pacing_rate | The rate the kernel is willing to send at |
wscale:7,7 | Window scale factors, send and receive. Missing means no scaling |
Turn cwnd into bytes: 1443 segments times 1448 bytes is about
2.1 MB in flight. Against a 179 MB BDP, this flow is using 1% of
the pipe, which matches the 108 Mbit/s delivery_rate.
Two readings distinguish the two causes:
cwndlarge,bytes_retransnear zero, but throughput low. The receive window is the limit. Look at the peer’srcv_spaceand itstcp_rmemceiling.cwndsmall andssthreshwell below it, withbytes_retransclimbing. Loss is the limit, and no amount of buffer will fix it.
Loss, and why it dominates on long paths
This course has already established the Mathis bound:
throughput ~ (MSS / RTT) x (1 / sqrt(p))
The consequence on a long path is severe, and it is worth computing rather than asserting:
awk 'BEGIN {
mss = 1448; rtt = 0.150
split("0.0001 0.001 0.01", losses, " ")
for (i = 1; i <= 3; i++) {
p = losses[i] + 0
printf "loss %6.4f%% ceiling %8.1f Mbit/s\n", \
p * 100, mss * 8 / rtt / sqrt(p) / 1e6
}
}'
loss 0.0100% ceiling 7.7 Mbit/s
loss 0.1000% ceiling 2.4 Mbit/s
loss 1.0000% ceiling 0.8 Mbit/s
A hundredth of a percent of loss caps a single flow at 7.7 Mbit/s on this path. One percent caps it at 0.8. The link rate never enters the calculation at all - which is why “we upgraded to 10 Gb and nothing got faster” is such a familiar report.
Compare the two bounds we now have for this path:
| Bound | Ceiling |
|---|---|
| Window: 6 MB over 150 ms RTT | 335 Mbit/s |
| Loss: Mathis at 0.01% | 7.7 Mbit/s |
Throughput is limited by whichever is lower, so on this path a
loss rate too small for a ping test to detect dominates
completely. Establish the loss rate before you tune buffers.
Raising tcp_rmem to 256 MB on a path with 0.01% loss moves the
window bound from 335 Mbit/s to 14 Gbit/s and changes the
achieved throughput by nothing at all, because the flow was never
against that bound.
Congestion control
sysctl net.ipv4.tcp_congestion_control
cat /proc/sys/net/ipv4/tcp_available_congestion_control
net.ipv4.tcp_congestion_control = cubic
cubic reno
cubic is the default nearly everywhere. It is loss-based:
it grows the window until a packet is dropped, then backs off.
That works well when loss means congestion, and works badly when
loss is caused by anything else - a lossy radio link, a policer,
or a cable with a marginal connector. On such a path, cubic
interprets every random drop as congestion and keeps its window
small.
bbr models the path instead, estimating bottleneck bandwidth
and minimum RTT and pacing to that estimate. It largely ignores
loss that is not accompanied by evidence of queueing, which is
why it can be dramatically faster on a long path with a low
background loss rate.
# bbr may need its module loaded before it appears as available.
sudo modprobe tcp_bbr
cat /proc/sys/net/ipv4/tcp_available_congestion_control
reno cubic bbr
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
This is a sender-side setting and takes effect on new
connections. It requires no change at the far end, which makes it
unusually easy to A/B test: set it, re-run the transfer, compare
delivery_rate.
Parallel streams: the workaround that works
When the path has loss you cannot fix and the transfer must go faster today, run several flows. Each is capped by the Mathis bound independently, so N flows get roughly N times the throughput:
iperf3 -c 198.51.100.24 -P 8 -t 30
This is what rsync cannot do and what modern object-storage
clients do by default. It is a workaround, not a fix - it
consumes N times the share of a congested link, so it is
antisocial on a shared path and entirely reasonable on a
dedicated one. Say which you are on before reaching for it.
Knowledge check
Knowledge check · 5 questions
Q1. A 10 Gb/s path has an RTT of 150 ms and a tcp_rmem maximum of 6 MB. Assuming zero packet loss, what single-flow throughput can you expect at best?
Q2. Which value should you raise to let autotuning fill a long fat path?
Q3. A ping test reporting 0% loss over 20 packets is sufficient evidence that packet loss is not limiting a bulk transfer.
Q4. A flow shows cwnd 1443, mss 1448, bytes_retrans 1024 and delivery_rate 108 Mbps on a 179 MB BDP path. Which conclusions are supported? Select all that apply.
Q5. On which path is switching from cubic to bbr most likely to help?
Passing score: 75%. Answers are checked in this browser.