Skip to main content

Congestion Control

Flow control protects the receiver. Congestion control protects the network - the routers and links between the two endpoints. When too much traffic converges on a link, router queues overflow and packets are dropped. No single receiver is out of room; the path itself is overloaded. TCP has no direct view into the network, so it infers congestion from loss and adjusts a second limit: the congestion window, cwnd.

The sender may send at most min(rwnd, cwnd) unacknowledged bytes. This lesson is about how cwnd moves.

Slow start

A new connection has no idea how much the path can carry, so it probes aggressively. Starting from a small cwnd, it doubles cwnd every RTT - 1, 2, 4, 8, 16. This exponential ramp is called slow start (slow only in that it starts small; it grows fast). It continues until cwnd reaches a threshold called ssthresh.

Worked example: slow start in segments

Say cwnd starts at 1 segment (roughly 1 MSS, ~1460 bytes) and ssthresh is set at 16 segments:

RTTcwnd at start of RTTcwnd after doubling
112
224
348
4816

At the start of RTT 5, cwnd = 16 has reached ssthresh, so the connection switches from doubling to congestion avoidance and now grows by only +1 segment per RTT: 17, 18, 19, and so on, until loss occurs.

Congestion avoidance (AIMD)

Past ssthresh, doubling would be reckless, so TCP switches to congestion avoidance: cwnd grows by just +1 per RTT - additive increase. This is the "AI" of AIMD (Additive Increase, Multiplicative Decrease). The sender gently pushes for more bandwidth, one segment at a time, until it finds the ceiling.

Reacting to loss

Loss is TCP's congestion signal. When it happens, TCP does a multiplicative decrease: it sets ssthresh = cwnd / 2. What happens to cwnd depends on how loss was detected:

Loss signalReaction
Timeout (no ACKs at all)cwnd drops to 1, restart slow start - the network may be badly congested
Triple duplicate ACK (data still flowing)cwnd drops to the new ssthresh - fast recovery, a milder signal

The sawtooth

Additive increase then multiplicative decrease, over and over, traces the classic TCP sawtooth: a steady linear climb, a sudden halving on loss, then another climb. Step through the rounds below and watch cwnd ramp in slow start, level into linear growth past ssthresh, and collapse on each loss.

slow startcongestion avoidanceloss
ssthresh 16RTT round
slow start (x2 / RTT)cwnd = 1ssthresh = 16
Below ssthresh: cwnd doubles every RTT - exponential probing for capacity.
round 0 / 15
info

rwnd (flow control) and cwnd (congestion control) are independent limits computed for different reasons - one guards the receiver's buffer, the other guards the network. The sender is bound by whichever is smaller at any moment.

Bufferbloat turns "more buffer" into worse loss detection

Routers along a path often carry deep queues to avoid dropping packets during bursts. That sounds helpful, but it means a link can be saturated for a long time before any packet actually gets dropped - so TCP's slow start keeps doubling cwnd well past the point the path can sustain, filling the queue deeper and deeper. By the time a drop finally signals congestion, the queue is full of stale packets, round-trip time has ballooned (every packet waits behind a huge queue), and the eventual multiplicative decrease has to unwind far more excess data than it would with a shallow queue. This "bufferbloat" is why latency on a saturated home connection can spike to seconds even though no packets are technically being dropped, and why algorithms like BBR try to infer congestion from RTT/delivery-rate changes instead of waiting for loss.