TCP buffer sizing on high-latency links
The bandwidth-delay product is the number people skip, and it is usually the one that explains why a link that benchmarks at 900 Mbit/s on the LAN tops out at 40 Mbit/s across an ocean.
Start from the product
In-flight data is what keeps a TCP connection busy. If the sender is allowed only a small window, it spends most of its time waiting for acknowledgements rather than transmitting. The ceiling is:
throughput_max = window / RTT
A 200 ms round trip and a 256 KB window give roughly 10 Mbit/s, no matter how fast the physical link is. To move 1 Gbit/s at that RTT you need about 25 MB in flight.
The three limits that actually bind
net.core.rmem_max/wmem_max— the hard ceiling a socket can request viaSO_RCVBUF.net.ipv4.tcp_rmem/tcp_wmem— the autotuning range, as min / default / max.- The application's own read and write loop. A process that reads 64 KB at a time and does something slow with it will never fill a large window.
Raising only the first two and leaving the application unchanged is the most common reason "the tuning did nothing".
Autotuning is usually enough
Modern kernels grow the receive window automatically. On a clean link, the default range is fine up to a few hundred Mbit/s. Where it stops working is when the kernel cannot distinguish "the peer is slow" from "the path is long", which is exactly the case on satellite, mobile, and congested transit.
For those paths, a wider autotuning ceiling is worth setting:
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_rmem = 4096 131072 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
Why the qdisc matters
BBR estimates bandwidth and RTT from delivery rate rather than treating loss as the only congestion signal, which helps on paths with a few percent loss. But it needs a queue discipline that supports pacing. fq does; fq_codel and pfifo_fast behave differently. Setting the congestion control without the qdisc is a common half-measure.
Measuring it properly
Single-stream iperf3 is the only number that matters here. Parallel streams hide the window limit by opening several connections, each with its own window, which is exactly the workaround you are trying to avoid.
iperf3 -c host -t 30 -O 5
Watch ss -tim on the receiver: cwnd and rwnd stuck well below the configured maximum means the limit is elsewhere — usually the application.