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:
| RTT | cwnd at start of RTT | cwnd after doubling |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 2 | 4 |
| 3 | 4 | 8 |
| 4 | 8 | 16 |
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 signal | Reaction |
|---|---|
| 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.
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.
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.