Bandwidth vs Latency
Bandwidth is how many bytes a second the pipe carries; latency is how long the first byte takes; transfer time is latency plus size over bandwidth, and a single TCP connection cannot use a fat, long pipe unless its window covers the bandwidth-delay product.
The problem
scp of a large file gets 480 Mbit/s and not one bit more; the link is 95 % idle. Meanwhile an API on the same link feels no faster than it did over 100 Mbit/s. Both complaints have the same explanation.Two independent numbers
Bandwidth is capacity: bits per second the link can carry. Latency is delay: the time for a bit to travel from one end to the other (one-way) or there and back (RTT). They are set by different things — bandwidth by the link technology and the narrowest hop on the path, latency by distance and queueing — and neither implies the other. A satellite link has high bandwidth and 600 ms RTT; a loopback interface has modest bandwidth and 20 µs RTT. A truck full of hard drives has enormous bandwidth and a latency of days.
The practical consequence: a workload of small request/response exchanges is bounded by latency and barely notices bandwidth, because each exchange waits a full RTT and sends a few hundred bytes. A bulk transfer is bounded by bandwidth — once it gets going. Upgrading a link from 100 Mbit/s to 10 Gbit/s makes the API feel exactly as slow, because a 1 kB request never used more than a sliver of either.
Transfer time = latency + size / bandwidth
The first-order model: the first byte arrives after one latency, and the rest stream in at the bandwidth. Time to move S bytes ≈ RTT + S / B (plus, in reality, the slow-start ramp described below). The two terms cross over at S = RTT × B — below that size latency dominates, above it bandwidth does.
Worked examples make the crossover visible. A 2 kB API response at 100 ms RTT on 1 Gbit/s: 100 ms + 0.016 ms — latency is 99.98 % of the time; the link speed is irrelevant. A 1 GB backup on the same link: 100 ms + 8 s — bandwidth is everything, and moving the peers closer saves nothing. A 1 MB web page sits near the crossover and is sensitive to both, which is why web performance work argues about both RTT reduction and compression.
| Size | 1 ms RTT, 1 Gbit/s | 100 ms RTT, 1 Gbit/s | 100 ms RTT, 100 Mbit/s | Dominated by |
|---|---|---|---|---|
| 2 kB (API call) | 1.02 ms | 100.02 ms | 100.2 ms | latency |
| 100 kB (image) | 1.8 ms | 100.8 ms | 108 ms | latency |
| 1 MB (page + assets) | 9 ms | 108 ms | 180 ms | both |
| 100 MB (video segment) | 801 ms | 900 ms | 8.1 s | bandwidth |
| 1 GB (backup) | 8.0 s | 8.1 s | 80 s | bandwidth |
The bandwidth-delay product
TCP only sends as much data as the receiver has advertised it can hold (Flow Control: The Receive Window) and as much as its congestion window allows (Congestion Control: Protecting the Network), then waits for acknowledgements — which take one RTT to come back. To keep a link full, the sender must have a full RTT’s worth of bytes in flight: that quantity is the bandwidth-delay product, BDP = bandwidth × RTT. A 10 Gbit/s link at 100 ms RTT holds 1 Gbit = 125 MB in flight. A 1 Gbit/s link at 100 ms holds 12.5 MB; at 1 ms, 125 kB.
If the window is smaller than the BDP, the sender fills it, stops, and idles until the first ACK returns; throughput is capped at window / RTT no matter what the link can do. The original TCP window field is 16 bits — 64 kB — which at 100 ms RTT caps a connection at 64 kB / 0.1 s = 5.2 Mbit/s. Window scaling (RFC 7323) raises the limit to 1 GB, and modern kernels autotune buffers, but only up to a configured maximum: Linux’s default net.ipv4.tcp_rmem maximum is 6 MB, which at 100 ms gives 6 MB / 0.1 s = 60 MB/s ≈ 480 Mbit/s — the number from the problem statement, on a link fifty times wider. Raising tcp_rmem/tcp_wmem maxima to the BDP, or using several connections in parallel (what download managers, aria2 and browsers do), is the fix.
Slow start compounds this on short transfers: cwnd begins around 10 segments (~14.6 kB) and doubles per RTT, so reaching a 12.5 MB window takes ~10 RTTs — a full second at 100 ms — during which the link is mostly idle. A transfer that finishes inside slow start never sees the bandwidth at all.
throughput ≤ window / RTT RTT 100 ms: window 64 kB → 64 kB / 0.1 s = 640 kB/s ≈ 5 Mbit/s (no window scaling) window 6 MB → 6 MB / 0.1 s = 60 MB/s ≈ 480 Mbit/s (Linux default tcp_rmem max) window 125 MB → 125 MB / 0.1 s = 1.25 GB/s ≈ 10 Gbit/s (window = BDP: link full) RTT 1 ms, window 6 MB: 6 MB / 0.001 s = 6 GB/s → link (1–10 Gbit/s) is the limit, not the window
What to do about each
Latency-bound workloads: cut round trips. Reuse connections (Keep-Alive and Connection Reuse, Connection Pooling), batch and pipeline requests, parallelise independent calls, move the peer closer (CDNs: The Networking View, regional deployment), use TLS 1.3 and session resumption to shave handshake trips. Compression barely helps: 2 kB or 1 kB is the same one RTT.
Bandwidth-bound workloads: size the TCP window to the BDP (buffer maxima on both ends), use parallel connections when a single one cannot be tuned, compress if the CPU is cheaper than the link, and remember that the bottleneck is the narrowest hop on the path, not the fastest NIC in the rack. And measure which one you are: a transfer that plateaus at a round number well below link speed on a long-RTT path is a window problem; one that plateaus at link speed is done.
- Small, chatty: latency-bound → fewer round trips, closer peers, warm connections.
- Large, bulk: bandwidth-bound → window ≥ BDP, parallel streams, the narrowest hop is the limit.
- Plateau at ~window/RTT on a long path = window too small; plateau at link rate = finished.
Key points
- Bandwidth is capacity (bits/s); latency is delay (s). They are independent: high bandwidth with high latency still makes small exchanges slow.
- Transfer time ≈ RTT + size/bandwidth; latency dominates below size = RTT × bandwidth, bandwidth above.
- BDP = bandwidth × RTT is the bytes that must be in flight to fill the link: 125 MB for 10 Gbit/s at 100 ms.
- TCP throughput ≤ window / RTT; 64 kB at 100 ms is ~5 Mbit/s; Linux’s default 6 MB maximum is ~480 Mbit/s.
- Slow start needs ~log2(BDP / initial cwnd) RTTs to open the window; short transfers never reach bandwidth.
- Fix latency-bound work with fewer round trips; fix bandwidth-bound work with bigger windows or parallel connections.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why does a faster link not make my API faster?
A 2 kB response occupies a 1 Gbit/s link for 16 µs and waits 100 ms for the round trip. Multiplying the link speed by ten removes 14 µs.
▸Why must TCP keep a whole RTT of data in flight?
Because acknowledgements take an RTT to return and the sender may not exceed its window without them. If the window empties before the first ACK arrives, the link sits idle until it does.
▸Why do download managers open several connections?
Each connection is capped at window/RTT. Four connections with the same window quadruple the in-flight bytes without touching kernel settings — and grab a larger share of a congested link, which is the less polite reason.
Bandwidth vs latency
How it fails
What the failure looks like from inside real software.
- Cross-region file sync plateaus at ~480 Mbit/s on a 10 Gbit/s link: receive buffer maximum (6 MB) below the 125 MB BDP; raise
tcp_rmem/tcp_wmemor parallelise. - Backup over a 64 kB fixed-window appliance takes hours: ~5 Mbit/s at 100 ms; no window scaling.
- Link upgraded 100 Mbit/s → 1 Gbit/s, dashboards show no API latency change: workload is latency-bound; round trips were the cost.
- Small object fetches from cross-continent storage are "slow despite the fat pipe": each object finishes inside slow start and pays a cold connection; pool connections and batch.
- Throughput test with one stream shows 20 % of link; ten parallel streams show 100 %: per-connection window limit, not the link.