The Runtime Theory
SystemArchitecturenetwork

What happens when TCP adjusts congestion?

A step-by-step walk of TCP's congestion control: slow start, the ACK clock, ssthresh, congestion avoidance, and what a lost packet does to the window.

The Runtime Theory Team1 min read05 steps

trace spine

  1. 01 Fresh connection starts slow start
  2. 02 ACK clock doubles the window
  3. 03 ssthresh switches to avoidance
  4. 04 Loss event halves cwnd
  5. 05 Recovery and steady state

TCP doesn't know the path's capacity — it has to discover it by probing. Congestion control is that probe, and it lives entirely in the kernel of both endpoints. The story is the window (cwnd): how many unacknowledged bytes the sender is willing to have in flight.

trace stepKernel

A fresh connection starts with cwnd = 10 segments (Linux default, 1460 bytes each — the RFC 6928 recommendation). The sender injects 10 segments (~14.6 KB) into the network without waiting for ACKs. This is slow start: the sender is deliberately under-utilizing the link until it learns the real capacity.

trace stepKernel

Each ACK that arrives increases cwnd by one segment. Since 10 segments were in flight, the first 10 ACKs double the window to 20, then 40, 80 — exponential growth, one round trip per doubling. On a 40 ms RTT link, cwnd passes 1 MB in about 7 RTTs (~280 ms). This is the ACK clock: the network's own acknowledgments pace the sender.

trace stepKernel

Growth is exponential until cwnd hits ssthresh (initialized to a huge value, often 2^31 or the configured max). Past ssthresh, the sender switches to congestion avoidance: one segment per RTT, linear growth. The kernel tracks this with a per-connection counter; nothing outside the kernel knows or cares.

trace stepKernel

Now the interesting event: a packet is lost. The sender detects it via three duplicate ACKs (fast retransmit) or a timeout. The response, per the classic Reno-style algorithm: ssthresh = cwnd / 2, cwnd drops to the new ssthresh, and the sender retransmits the missing segment. A 1 MB window halves to 500 KB in one event — a single loss can erase half a second of slow-start growth.

bash
ss -tin | grep -E "cwnd|cubic|bbr"
trace stepKernel

The connection settles into the sawtooth: linear growth until a loss, halving, growth again. In steady state a long-lived flow spends most of its time near its loss-triggered ceiling. The observed throughput roughly follows cwnd / RTT — which is why the same connection is 10× faster on a 10 ms RTT link than a 100 ms one even with identical cwnd.

What the machine actually does is a closed feedback loop: send, count ACKs, double, probe, halve. The entire algorithm is a handful of integers (cwnd, ssthresh, RTT) updated on every ACK in the hot path — nanoseconds of work per packet that collectively decide how fast the internet lets you go.