Congestion Control
TCP congestion control is how a sender fills a pipe it cannot see. This lesson is textbook Reno: start with one packet, double the window each round-trip, then creep by one, and back off when something spills. The picture is a sawtooth, not a flat line at "the right rate."
You do not get the bottleneck's numbers. This path drains 8 packets per RTT and has a 4-packet buffer. The sender holds two local integers instead: cwnd (how many MSS may be in flight) and ssthresh (where doubling stops). A triple-duplicate ACK means the path is still delivering, so Reno halves cwnd and stays in Congestion Avoidance. In the timeout preset no ACK comes back, so cwnd goes to 1 and Slow Start restarts.
How TCP congestion control works
After the handshake, a Reno sender holds those two integers. Every lossless RTT it asks one question: is cwnd still below ssthresh?
If yes, Slow Start: cwnd = min(cwnd * 2, ssthresh). Real stacks do this per ACK (cwnd += 1 for each ACK in the flight), which approximates this doubling when a full window is acknowledged packet by packet; delayed ACKs and byte counting affect real growth. This lesson batches the flight into one update so one beat is one RTT.
If no, Congestion Avoidance: cwnd += 1. Linear. Boring on purpose. Doubling is over; now you are feeling for the last free slot in the buffer.
A flight of cwnd packets overflows when cwnd > C + B. C is the bottleneck's drain rate in packets per RTT, which is also the bandwidth-delay product on a 1-RTT path. B is the buffer. Everything that fits beyond C sits in the queue; the count beyond C + B is dropped. This batched model selects the loss feedback separately; it does not derive three duplicate ACKs from a literal tail drop in that same flight. The sender never reads C. It only reads ACKs, or the lack of them.
How the sender hears that drop decides the next cwnd:
- Triple-duplicate ACK (Fast Retransmit): some packets of the window got through, so the path is wounded, not dead. Reno sets
ssthresh = max(cwnd // 2, 2)andcwnd = ssthresh, and stays in Congestion Avoidance. That is multiplicative decrease. This lesson skips Fast Recovery's extrassthresh + 3. - Timeout: the retransmission timer expires before the required acknowledgment arrives; this preset models a silent flight. Reno still sets
ssthresh = max(cwnd // 2, 2)from the lost flight, butcwndgoes to 1 and Slow Start restarts.
AIMD is the pair: additive increase (+1 / RTT) and multiplicative decrease (× ½). Additive decrease would leave the buffer full and growth back to 13 would overflow again. Multiplicative decrease is what opens headroom.
TCP congestion control Step by Step
The default run is Sawtooth: C = 8 packets/RTT, B = 4, initial cwnd = 1, initial ssthresh = 8. Loss is not scripted. It happens when a flight no longer fits.
- Init. Pipe empty. Chart point at
(RTT 0, cwnd 1). Thessthreshguide sits at 8 and is labelled sender-local. TheCguide also sits at 8 and is labelled hidden from the sender. The two numbers match on this run so both rules appear before the first drop — they are not the same variable, and the sender does not knowC. - RTT, Slow Start. Send 1, ACK 1.
cwnd 1 → 2. - RTT, Slow Start. Send 2, ACK 2.
cwnd 2 → 4. - RTT, Slow Start. Send 4, ACK 4.
cwnd 4 → 8, clamped atssthresh. The point lands on the dashed line. - Hit ssthresh. The rule flips. Next growth is
+1, not×2. No packets move on this beat. - RTT, CA. Send 8, ACK 8. Queue 0 — the pipe is exactly full.
cwnd 8 → 9. - RTT, CA. Send 9. Queue 1.
cwnd 9 → 10. - RTT, CA. Send 10. Queue 2.
cwnd 10 → 11. - RTT, CA. Send 11. Queue 3.
cwnd 11 → 12. - RTT, CA. Send 12. Queue 4/4.
cwnd 12 → 13. The buffer is full; one more packet will not fit. - Overflow. Send 13. Twelve fit, one drops. Three duplicate ACKs come back.
cwndstays 13 — the cut is the next beat. - Cut.
ssthresh = max(13 // 2, 2) = 6,cwnd = 6. State stays Congestion Avoidance. The chart point drops vertically; thessthreshguide slides 8 → 6. - RTT, CA. Send 6.
cwnd 6 → 7. The first+1after the cut, not a doubling. That is this lesson's simplified Reno recovery — we skip Fast Recovery'sssthresh + 3. - RTT, CA. Send 7.
cwnd 7 → 8. - RTT, CA. Send 8.
cwnd 8 → 9. Back at the bandwidth-delay product. - Saw. The second tooth is drawn in one beat:
9 → 10 → 11 → 12 → 13, overflow, drop to 6. AIMD repeats. - Done.
cwnd = 6,ssthresh = 6. The chart holds a Slow Start ramp and two CA teeth. AIMD did not discover one exact capacity. It found a usable range and will keep probing through it: climb, overflow, halve, repeat.
Switch to No loss and the same climb runs with B = 16. Nothing overflows. The run stops at cwnd = 14 still adding 1. Without a drop the sender never shrinks.
Switch to Timeout and the overload at 13 is silent: the flight vanishes, no ACK returns, RTO fires. The cut writes cwnd = 1 and Slow Start restarts: 1 → 2 → 4 → 6 (clamped at the new ssthresh), then CA again. Same overload, different loss kind, a hole punched in the window instead of a notch.
On the canvas the pipe is why this RTT dropped; the chart is every RTT stacked. Watch the point walk the exponential, then the staircase, then fall. That fall is AIMD, not a decoration.
Why the window sawtooth
A sender that knew C would sit at cwnd = C and be done. It does not know C. Slow Start reaches a window of N in Θ(log N) RTTs (1 → 2 → 4 → 8 takes three RTTs). Congestion Avoidance then pays one RTT per extra MSS, so growing N more packets is Θ(N) RTTs — that is why the rising edge of each tooth is long and straight.
The falling edge is the information. Halving on a drop leaves the buffer mostly empty, so the next climb has room to probe again. If Reno subtracted 1 instead, cwnd would go 13 → 12, the queue would stay 4/4, and after a successful flight of 12, growth to 13 would overflow again. AIMD is a simple rule that both uses the pipe and backs off hard enough to share it.
The second tooth on the default run is there so you see the rule, not a one-off accident. Same +1 until C + B, same × ½.
The three presets are signatures you can read on a live window:
- Linear climb, then a half-window drop: loss-based probing, usually a triple-dupACK.
- A collapse to 1, then Slow Start again: a timeout, or feedback that went silent.
- The queue keeps growing and the window never falls: a buffer deep enough that nothing drops — bufferbloat.
Cubic and BBR paint different curves. The three signatures above are what a loss-based Reno sawtooth looks like.
TCP Reno edge cases
- Initial
ssthreshis a teaching choice. Real stacks often startssthreshat the advertised window, so the first loss is what ends Slow Start. This lesson startsssthresh = 8so you see both rules before the first drop. ssthreshnever falls below 2.max(cwnd // 2, 2)keeps a degenerate window from collapsing to zero.- Slow Start can land exactly on
ssthresh. Growing4withssthresh = 8produces 8, not 16. Growing4withssthresh = 6after a timeout clamps to 6, then CA takes over. - A timeout is not a triple-dupACK. Same
cwnd = 13overload; one path keeps 6, the other goes to 1. If you remember one distinction from Reno, remember that one. - No loss means no cut. CA keeps increasing
cwndwhile the model observes no loss; any finite buffer eventually overflows if this growth continues. Bufferbloat is this edge case wearing a longer queue.
Common mistakes in TCP congestion control
- Treating Slow Start as "until the first loss." That is how a stack with a huge initial
ssthreshbehaves. The actual predicate iscwnd < ssthresh. This run flips at 8, then loses at 13. - Doubling after a triple-dupACK. This lesson's Reno cut stays in Congestion Avoidance. Doubling after the cut is Tahoe-with-the-labels-peeled-off.
- Reading
ackgrowth as congestion control. The handshake'sack = seq + 1is numbering.cwndis how many of those numbered bytes may be in flight. Different knobs. - Calling every drop a timeout. If three duplicate ACKs arrived, the receiver is alive and still seeing later packets. That is why Reno keeps half the window.
- Thinking AIMD is fair because it is polite. Two AIMD flows converge toward equal share only in an idealized model — equal RTTs, shared loss feedback; that is a different picture (Chiu–Jain) and is not this lesson. One flow's sawtooth is already the whole mechanism.
- Reading the sawtooth as "it found C." The sender is still probing a range. The next climb will overflow again.
What this Reno model leaves out
This is per-RTT Reno on one flow. Real TCP updates per ACK, starts with a larger initial window (often 10 MSS), and Fast Recovery briefly sets cwnd = ssthresh + 3 to account for the three packets that the duplicate ACKs represent. Cubic, BBR, pacing, delayed ACKs, SACK, and two-flow fairness are omitted so the sawtooth stays a sawtooth. The trace illustrates Reno’s growth and backoff pattern. Real Reno computes the loss threshold from FlightSize, handles duplicate ACKs and recovery explicitly, and is also limited by the receiver window; the model uses its full-flight cwnd, batches ACKs, and skips the temporary +3.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Premium players need paid access. Every Explain doc is free.
Found a mistake or something unclear? Leave feedback.