Reading TCP congestion control from the kernel
Most explanations of TCP congestion control start with the AIMD diagram and stop there. That is enough to pass an interview, but it does not explain why a link that measures 200 Mbit/s on a speed test stalls at 30 Mbit/s when you pull a single large file. To understand that, you have to look at what the kernel actually does with the congestion window.
The four values that matter
On Linux, three sysctl values and one module decide most of your throughput:
net.ipv4.tcp_congestion_control
net.core.default_qdisc
net.ipv4.tcp_rmem
net.ipv4.tcp_wmem
The first two pick the algorithm and the queue discipline in front of it. The last two set the receive and send buffer ranges, and they are the ones people get wrong.
Why rmem_max overrides tcp_rmem
This is the part that surprises people. tcp_rmem is a three-tuple of minimum, default, and maximum autotuning values. But the autotuner will never grow a buffer past net.core.rmem_max. So if you set:
net.ipv4.tcp_rmem = 4096 131072 6291456
net.core.rmem_max = 212992
the effective ceiling is 208 KB, not 6 MB. The larger number in tcp_rmem is decoration.
Work out what that ceiling costs you. The bandwidth-delay product tells you how much data has to be in flight to keep a pipe full:
BDP = bandwidth x RTT
At 50 ms RTT and a 208 KB window, the best a single stream can do is:
208 KB / 0.05 s = 4.16 MB/s = 33 Mbit/s
That number should look familiar. It is roughly where single-stream transfers plateau on a lot of VPS links, and it has nothing to do with the provider throttling you.
Fixing it
Raise the ceiling and let the autotuner work:
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 1048576 33554432
net.ipv4.tcp_wmem = 4096 1048576 33554432
Two details worth noting. First, the middle value matters more than people expect: it is what the connection starts with, and a larger starting value shortens the slow-start ramp. Second, 64 MB of maximum is not 64 MB of actual memory — buffers only grow when the window needs them.
Slow start after idle
Long-lived connections that go quiet reset their congestion window, which means every burst after an idle period starts from scratch. For connection-pooled proxies and keepalive HTTP this is pure loss:
net.ipv4.tcp_slow_start_after_idle = 0
Verifying
Do not trust a browser download. Use iperf3 and test both directions separately, because the receive path and the send path are tuned by different sysctls and a one-directional test will hide half the problem:
iperf3 -c host -t 10 # upload
iperf3 -c host -t 10 -R # download
If upload improves and download does not, you changed tcp_wmem but left rmem_max alone.