Why congestion control?
Sliding windows let the sender have several segments in flight at once, and flow control keeps the receiver from being flooded. But the network in between has limits too. If every sender pushes as fast as it can, router queues overflow, packets are dropped, senders retransmit, and the internet can grind to a halt. This actually happened in October 1986, when throughput between two nearby Berkeley sites fell from 32 kbit/s to 40 bit/s: a congestion collapse.
Van Jacobson’s fix, now built into every TCP stack, adds a second window. The congestion window (cwnd) is how much data the sender allows itself in flight, based on what the network seems able to handle. The sender may send min(cwnd, receiver window) per round trip.
The four rules
TCP measures cwnd in MSS (maximum segment size) and changes it once per round trip (RTT):
- Slow start (cwnd < ssthresh): every ACK adds one MSS, so cwnd doubles every RTT: 1, 2, 4, 8, … Despite the name, the growth is exponential. It only starts small.
- Congestion avoidance (cwnd ≥ ssthresh): add just 1 MSS per RTT: 16, 17, 18, … carefully probing for spare capacity.
- Timeout (no ACK in time, so probably heavy congestion): ssthresh ← cwnd / 2 and cwnd ← 1. Back to slow start.
- Three duplicate ACKs (one segment lost, but later ones still arrive): ssthresh ← cwnd / 2, then
- TCP Tahoe: cwnd ← 1, the same as a timeout;
- TCP Reno: cwnd ← ssthresh (fast recovery), then continue linearly.
ssthresh (the slow-start threshold) is TCP’s memory of “where trouble started last time”.
Worked example
ssthresh starts at 16. There is a timeout in round 8 and 3 duplicate ACKs in round 17, using TCP Reno:
| Round | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | … | 17 | 18 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cwnd | 1 | 2 | 4 | 8 | 16 | 17 | 18 | 19 | 1 | 2 | 4 | 8 | 9 | … | 13 | 6 |
| ssthresh | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 9 | 9 | 9 | 9 | 9 | … | 9 | 6 |
- Rounds 1–4: slow start doubles the window, and in round 5 it reaches ssthresh = 16.
- Rounds 5–8: congestion avoidance adds 1 per round.
- Round 8: timeout at cwnd = 19, so ssthresh = 9 and cwnd = 1.
- Rounds 9–13: slow start again, capped at the new ssthresh of 9, then linear.
- Round 17: 3 duplicate ACKs at cwnd = 13, so ssthresh = 6. Reno continues from 6, while Tahoe would restart from 1.
(In this model, doubling stops exactly at ssthresh. Real stacks count per ACK and may slightly overshoot.)
The sawtooth and fairness
Long-running TCP connections settle into a sawtooth: the window climbs linearly until a loss, halves, and climbs again. This AIMD behaviour (additive increase, multiplicative decrease) has a nice property: when several connections share a link, they converge towards equal shares of the bandwidth.
Code
def simulate(ssthresh, rounds, events, reno=True):
cwnd, history = 1, []
for r in range(1, rounds + 1):
history.append(cwnd)
event = events.get(r)
if event: # loss in this round
ssthresh = max(2, cwnd // 2)
cwnd = 1 if event == "timeout" or not reno else ssthresh
elif cwnd < ssthresh:
cwnd = min(2 * cwnd, ssthresh) # slow start
else:
cwnd += 1 # congestion avoidance
return history
print(simulate(16, 18, {8: "timeout", 17: "dup"}))
# [1, 2, 4, 8, 16, 17, 18, 19, 1, 2, 4, 8, 9, 10, 11, 12, 13, 6]
Beyond Reno
| Variant | Key idea | Used by |
|---|---|---|
| Tahoe (1988) | Slow start, congestion avoidance, fast retransmit | historical |
| Reno (1990) | + fast recovery after 3 duplicate ACKs | textbooks, older systems |
| NewReno | Handles several losses in one window better | many systems |
| CUBIC | Window grows as a cubic function of time since the last loss | Linux, Windows, macOS default |
| BBR | Models bandwidth and RTT instead of reacting to loss | Google, YouTube |
Common mistakes
- Confusing the congestion window (protects the network) with the receiver window (protects the receiver). The sender uses the smaller of the two.
- Thinking slow start is slow. It grows exponentially.
- After a timeout, setting ssthresh to half of the old ssthresh instead of half of the current cwnd.
- Treating 3 duplicate ACKs like a timeout in Reno. Only Tahoe drops cwnd to 1 there.
Complexity at a glance
| Case / operation | Time | Why |
|---|---|---|
| Slow start | doubles per RTT | Exponential growth until cwnd reaches ssthresh. |
| Congestion avoidance | +1 MSS per RTT | Additive increase. |
| After a loss | cwnd / 2 | Multiplicative decrease (to 1 after a timeout). |
| Rounds to reach a window of W from 1 | about log₂ W | Thanks to slow start. |
Quick check
Test yourself — pick an answer to see if you got it.
1. The congestion window is 8 MSS and ssthresh is 16. What is cwnd after the next RTT without loss?
cwnd < ssthresh means slow start, so the window doubles from 8 to 16.
2. cwnd is 20 MSS when a timeout occurs. What are the new ssthresh and cwnd?
A timeout is the strongest signal of congestion. ssthresh becomes half the window (10) and cwnd restarts at 1.
3. Why does TCP Reno not reset cwnd to 1 after 3 duplicate ACKs?
The receiver is still getting data, so Reno halves the window (fast recovery) instead of starting over like Tahoe.
4. What does AIMD stand for?
TCP grows the window by a constant (+1 MSS per RTT) and shrinks it by a factor (÷ 2) after a loss, which is fair and stable.