TCP, kernel bypass and the latency tiers
What does TCP do under load, and what does each tier of the network cost per round trip?
TCP: slow start, congestion control, incast
Slow start
A new connection knows nothing about available capacity, and it cannot ask. So slow start begins with a small congestion window (cwnd: how much unacknowledged data it may have in flight) of about 10 segments ≈ 14.6 KB, and doubles it every RTT.
Example: from 14.6 KB to 125 MB takes log₂(125,000,000 / 14,600) ≈ 13–14 doublings ≈ 13–14 RTTs ≈ 1.4 seconds at 100 ms RTT. Short transfers never leave slow start, so their effective speed depends on RTT and transfer size, not link speed. This is why connection reuse and HTTP/2 multiplexing matter much more than they seem to.
- a small congestion window of about 10 segments ≈ 14.6 KB
- bytes in flight needed to fill the link (the bandwidth-delay product)
- time for a message to reach the other side and the answer to come back
Data table
| Window W | series | Time to reach it |
|---|---|---|
| 19.5 KB | RTT = 10 ms | 4.54 ms |
| 22.4 KB | RTT = 10 ms | 6.49 ms |
| 25.6 KB | RTT = 10 ms | 8.44 ms |
| 29.3 KB | RTT = 10 ms | 10.4 ms |
| 33.5 KB | RTT = 10 ms | 12.3 ms |
| 38.4 KB | RTT = 10 ms | 14.3 ms |
| 44 KB | RTT = 10 ms | 16.2 ms |
| 50.3 KB | RTT = 10 ms | 18.2 ms |
| 57.6 KB | RTT = 10 ms | 20.1 ms |
| 66 KB | RTT = 10 ms | 22.1 ms |
| 75.5 KB | RTT = 10 ms | 24.1 ms |
| 86.5 KB | RTT = 10 ms | 26 ms |
| 99 KB | RTT = 10 ms | 28 ms |
| 113 KB | RTT = 10 ms | 29.9 ms |
| 130 KB | RTT = 10 ms | 31.9 ms |
| 149 KB | RTT = 10 ms | 33.8 ms |
| 170 KB | RTT = 10 ms | 35.8 ms |
| 195 KB | RTT = 10 ms | 37.7 ms |
| 223 KB | RTT = 10 ms | 39.7 ms |
| 255 KB | RTT = 10 ms | 41.6 ms |
| 292 KB | RTT = 10 ms | 43.6 ms |
| 334 KB | RTT = 10 ms | 45.5 ms |
| 383 KB | RTT = 10 ms | 47.5 ms |
| 438 KB | RTT = 10 ms | 49.4 ms |
| 502 KB | RTT = 10 ms | 51.4 ms |
| 574 KB | RTT = 10 ms | 53.3 ms |
| 657 KB | RTT = 10 ms | 55.3 ms |
| 753 KB | RTT = 10 ms | 57.2 ms |
| 862 KB | RTT = 10 ms | 59.2 ms |
| 987 KB | RTT = 10 ms | 61.1 ms |
| 1.1 MB | RTT = 10 ms | 63.1 ms |
| 1.26 MB | RTT = 10 ms | 65 ms |
| 1.45 MB | RTT = 10 ms | 67 ms |
| 1.65 MB | RTT = 10 ms | 68.9 ms |
| 1.89 MB | RTT = 10 ms | 70.9 ms |
| 2.17 MB | RTT = 10 ms | 72.8 ms |
| 2.48 MB | RTT = 10 ms | 74.8 ms |
| 2.84 MB | RTT = 10 ms | 76.7 ms |
| 3.25 MB | RTT = 10 ms | 78.7 ms |
| 3.73 MB | RTT = 10 ms | 80.6 ms |
| 4.26 MB | RTT = 10 ms | 82.6 ms |
| 4.88 MB | RTT = 10 ms | 84.5 ms |
| 5.59 MB | RTT = 10 ms | 86.5 ms |
| 6.4 MB | RTT = 10 ms | 88.4 ms |
| 7.33 MB | RTT = 10 ms | 90.4 ms |
| 8.39 MB | RTT = 10 ms | 92.3 ms |
| 9.6 MB | RTT = 10 ms | 94.3 ms |
| 11 MB | RTT = 10 ms | 96.2 ms |
| 12.6 MB | RTT = 10 ms | 98.2 ms |
| 14.4 MB | RTT = 10 ms | 100 ms |
| 16.5 MB | RTT = 10 ms | 102 ms |
| 18.9 MB | RTT = 10 ms | 104 ms |
| 21.6 MB | RTT = 10 ms | 106 ms |
| 24.7 MB | RTT = 10 ms | 108 ms |
| 28.3 MB | RTT = 10 ms | 110 ms |
| 32.4 MB | RTT = 10 ms | 112 ms |
| 37.1 MB | RTT = 10 ms | 114 ms |
| 42.5 MB | RTT = 10 ms | 116 ms |
| 48.7 MB | RTT = 10 ms | 118 ms |
| 55.7 MB | RTT = 10 ms | 120 ms |
| 63.8 MB | RTT = 10 ms | 122 ms |
| 73 MB | RTT = 10 ms | 124 ms |
| 83.6 MB | RTT = 10 ms | 126 ms |
| 95.7 MB | RTT = 10 ms | 127 ms |
| 110 MB | RTT = 10 ms | 129 ms |
| 125 MB | RTT = 10 ms | 131 ms |
| 144 MB | RTT = 10 ms | 133 ms |
| 164 MB | RTT = 10 ms | 135 ms |
| 188 MB | RTT = 10 ms | 137 ms |
| 215 MB | RTT = 10 ms | 139 ms |
| 247 MB | RTT = 10 ms | 141 ms |
| 282 MB | RTT = 10 ms | 143 ms |
| 323 MB | RTT = 10 ms | 145 ms |
| 370 MB | RTT = 10 ms | 147 ms |
| 424 MB | RTT = 10 ms | 149 ms |
| 485 MB | RTT = 10 ms | 151 ms |
| 555 MB | RTT = 10 ms | 153 ms |
| 636 MB | RTT = 10 ms | 155 ms |
| 728 MB | RTT = 10 ms | 157 ms |
| 833 MB | RTT = 10 ms | 159 ms |
| 954 MB | RTT = 10 ms | 161 ms |
| 19.5 KB | RTT = 100 ms | 45.4 ms |
| 22.4 KB | RTT = 100 ms | 64.9 ms |
| 25.6 KB | RTT = 100 ms | 84.4 ms |
| 29.3 KB | RTT = 100 ms | 104 ms |
| 33.5 KB | RTT = 100 ms | 123 ms |
| 38.4 KB | RTT = 100 ms | 143 ms |
| 44 KB | RTT = 100 ms | 162 ms |
| 50.3 KB | RTT = 100 ms | 182 ms |
| 57.6 KB | RTT = 100 ms | 201 ms |
| 66 KB | RTT = 100 ms | 221 ms |
| 75.5 KB | RTT = 100 ms | 241 ms |
| 86.5 KB | RTT = 100 ms | 260 ms |
| 99 KB | RTT = 100 ms | 280 ms |
| 113 KB | RTT = 100 ms | 299 ms |
| 130 KB | RTT = 100 ms | 319 ms |
| 149 KB | RTT = 100 ms | 338 ms |
| 170 KB | RTT = 100 ms | 358 ms |
| 195 KB | RTT = 100 ms | 377 ms |
| 223 KB | RTT = 100 ms | 397 ms |
| 255 KB | RTT = 100 ms | 416 ms |
| 292 KB | RTT = 100 ms | 436 ms |
| 334 KB | RTT = 100 ms | 455 ms |
| 383 KB | RTT = 100 ms | 475 ms |
| 438 KB | RTT = 100 ms | 494 ms |
| 502 KB | RTT = 100 ms | 514 ms |
| 574 KB | RTT = 100 ms | 533 ms |
| 657 KB | RTT = 100 ms | 553 ms |
| 753 KB | RTT = 100 ms | 572 ms |
| 862 KB | RTT = 100 ms | 592 ms |
| 987 KB | RTT = 100 ms | 611 ms |
| 1.1 MB | RTT = 100 ms | 631 ms |
| 1.26 MB | RTT = 100 ms | 650 ms |
| 1.45 MB | RTT = 100 ms | 670 ms |
| 1.65 MB | RTT = 100 ms | 689 ms |
| 1.89 MB | RTT = 100 ms | 709 ms |
| 2.17 MB | RTT = 100 ms | 728 ms |
| 2.48 MB | RTT = 100 ms | 748 ms |
| 2.84 MB | RTT = 100 ms | 767 ms |
| 3.25 MB | RTT = 100 ms | 787 ms |
| 3.73 MB | RTT = 100 ms | 806 ms |
| 4.26 MB | RTT = 100 ms | 826 ms |
| 4.88 MB | RTT = 100 ms | 845 ms |
| 5.59 MB | RTT = 100 ms | 865 ms |
| 6.4 MB | RTT = 100 ms | 884 ms |
| 7.33 MB | RTT = 100 ms | 904 ms |
| 8.39 MB | RTT = 100 ms | 923 ms |
| 9.6 MB | RTT = 100 ms | 943 ms |
| 11 MB | RTT = 100 ms | 962 ms |
| 12.6 MB | RTT = 100 ms | 982 ms |
| 14.4 MB | RTT = 100 ms | 1 s |
| 16.5 MB | RTT = 100 ms | 1.02 s |
| 18.9 MB | RTT = 100 ms | 1.04 s |
| 21.6 MB | RTT = 100 ms | 1.06 s |
| 24.7 MB | RTT = 100 ms | 1.08 s |
| 28.3 MB | RTT = 100 ms | 1.1 s |
| 32.4 MB | RTT = 100 ms | 1.12 s |
| 37.1 MB | RTT = 100 ms | 1.14 s |
| 42.5 MB | RTT = 100 ms | 1.16 s |
| 48.7 MB | RTT = 100 ms | 1.18 s |
| 55.7 MB | RTT = 100 ms | 1.2 s |
| 63.8 MB | RTT = 100 ms | 1.22 s |
| 73 MB | RTT = 100 ms | 1.24 s |
| 83.6 MB | RTT = 100 ms | 1.26 s |
| 95.7 MB | RTT = 100 ms | 1.27 s |
| 110 MB | RTT = 100 ms | 1.29 s |
| 125 MB | RTT = 100 ms | 1.31 s |
| 144 MB | RTT = 100 ms | 1.33 s |
| 164 MB | RTT = 100 ms | 1.35 s |
| 188 MB | RTT = 100 ms | 1.37 s |
| 215 MB | RTT = 100 ms | 1.39 s |
| 247 MB | RTT = 100 ms | 1.41 s |
| 282 MB | RTT = 100 ms | 1.43 s |
| 323 MB | RTT = 100 ms | 1.45 s |
| 370 MB | RTT = 100 ms | 1.47 s |
| 424 MB | RTT = 100 ms | 1.49 s |
| 485 MB | RTT = 100 ms | 1.51 s |
| 555 MB | RTT = 100 ms | 1.53 s |
| 636 MB | RTT = 100 ms | 1.55 s |
| 728 MB | RTT = 100 ms | 1.57 s |
| 833 MB | RTT = 100 ms | 1.59 s |
| 954 MB | RTT = 100 ms | 1.61 s |
Loss-based control must overshoot
Reno / CUBIC (the classic algorithms) use packet loss as the capacity signal. The surprising result: the algorithm must overshoot to learn. It fills the router's queue until it overflows, then slows down. So it normally runs with full buffers, which causes bufferbloat: 100 ms to 1 s+ of extra latency from queueing alone, on a link with no packet loss. Larger buffers made this worse, not better.
BBR (Google's algorithm) estimates the bottleneck bandwidth and the minimum RTT directly, sends at that rate, and does not fill queues. It is much better on long fat networks and lossy links. It is not free: BBRv1 was aggressive towards CUBIC flows. It marks the shift from loss as signal to model as signal.
Incast: the failure that matters most
Incast has exactly the shape of every distributed query: N workers reply at the same time to one coordinator (a synchronized many-to-one fan-in). This is Meera's problem.
- The switch port facing the coordinator has a small buffer (often a few hundred KB). N simultaneous replies overflow it and packets are dropped.
- Normally TCP recovers with fast retransmit (the receiver sees a gap because later packets still arrive). But replies are small, so there are no later packets to reveal the gap.
- So senders wait for the RTO (retransmission timeout), historically 200 ms – 1 s, when the datacenter RTT is ~100 µs.
One lost packet costs a 1,000× delay. Throughput does not fall slowly; it collapses by about 10× once the number of senders passes a threshold.
Mitigations:
- microsecond-level RTO timers;
- ECN / DCTCP (switches mark packets before dropping them, and senders react in proportion);
- staggered / jittered requests (small random delays, so replies do not arrive together);
- limit the fan-out width.
Jitter works by destroying synchronization — the same idea as the variance-reduction methods in the CPU part.
Kernel bypass
- Same arithmetic as the storage finding. The kernel network path costs ~5–15 µs: thousands of cycles of interrupt, context switch, copies and cache pollution. When the wire RTT was 500 µs, that was noise. At 100 Gb/s and ~2 µs RDMA or kernel-bypass RTT, software is the main cost. Cause 3 moved up again.
- DPDK (Data Plane Development Kit): user-space drivers that poll the card, with no interrupts, huge pages and pinned cores. ~5 µs → under 1 µs, with near line-rate small-packet processing. The price is real and often decisive: one core spins at 100% forever, you lose the whole kernel stack (TCP, firewall, tools), and the card is dedicated.
- RDMA (Remote Direct Memory Access) is more interesting: it reads and writes the memory of another machine directly, without using that machine's CPU. ~1–2 µs end to end. Remote memory then costs about the same as a remote NUMA access in CPU and memory, which made disaggregated memory and CXL-over-network realistic.
- Rule for when it matters: only when the work per message is similar to the ~5–15 µs stack cost. Analytics moving 8 MB Parquet chunks? Irrelevant — you are bandwidth-bound. Distributed transactions, consensus, shuffles of tiny messages? Decisive. For most teams the middle path is io_uring + TSO/GRO + interrupt tuning, which gives much of the benefit and keeps the kernel.
The latency tiers
This table is the network's "numbers every programmer should know". You will use it often.
| Tier | RTT | Bandwidth | Reason for the minimum |
|---|---|---|---|
| Same host, loopback | 10–30 µs | tens of GB/s | software path only |
| Same host, shared memory | ~0.1 µs | ~memory bandwidth | no protocol |
| Same rack (ToR = top-of-rack switch) | 50–200 µs | 10–100 Gb/s | 2 switch hops + stack |
| Same AZ / datacenter | 200–500 µs | 10–100 Gb/s | 4–6 hops, ~1 km |
| Cross-AZ (same region) | 0.5–2 ms | 10–100 Gb/s | 10–100 km of fiber + hops |
| Cross-region, same continent | 10–40 ms | varies | ~1,000 km+ |
| Cross-region, intercontinental | 70–250 ms | varies | speed of light, hard minimum |
| Object storage GET (same region) | 20–100 ms | ~50–90 MB/s per connection | not distance: service depth |
- Each tier is about 10× the previous one, a useful mental ruler: same host ~10 µs, same rack ~100 µs, same AZ ~500 µs, cross-AZ ~1 ms, cross-region ~100 ms.
- The last row deserves attention. Cross-AZ RTT is ~1 ms, but an S3 GET in the same region takes 20–100 ms. The extra 20–100× is not distance. It is load balancers, TLS handshake, authentication, metadata lookup and internal replication. The minimum is service depth, not travel time. Two different mechanisms with the same units. Mixing them up leads people to "fix" S3 latency by moving servers closer, which does not help.
- Synchronous replication across AZs costs ~1–2 ms per round trip. This is the numeric basis of the multi-AZ trade-off, and why quorum systems (which need a majority of replicas to agree) keep replicas inside one region and replicate asynchronously between regions.
- Synchronous consensus across regions is impossible for interactive work. Mumbai ↔ Virginia is ~200 ms RTT, and Paxos/Raft (consensus protocols) need 1–2 round trips per commit. This is geometry, not tuning. It is why Google Spanner uses atomic clocks: TrueTime lets it skip one coordination round trip, and pay a commit-wait limited by clock uncertainty rather than by distance.
The shape of the communication
The shape of the communication matters more than its volume. All-to-all shuffle is costly because it breaks the oversubscription bet. Synchronized fan-in causes incast because it is synchronized. Chatty chains are slow because RTT cannot be amortized. The same bytes can cost very different amounts depending only on the pattern.