Best on a larger screen — the animation and controls have more room to breathe.
Inputs

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) and cwnd = ssthresh, and stays in Congestion Avoidance. That is multiplicative decrease. This lesson skips Fast Recovery's extra ssthresh + 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, but cwnd goes 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.

  1. Init. Pipe empty. Chart point at (RTT 0, cwnd 1). The ssthresh guide sits at 8 and is labelled sender-local. The C guide 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 know C.
  2. RTT, Slow Start. Send 1, ACK 1. cwnd 1 → 2.
  3. RTT, Slow Start. Send 2, ACK 2. cwnd 2 → 4.
  4. RTT, Slow Start. Send 4, ACK 4. cwnd 4 → 8, clamped at ssthresh. The point lands on the dashed line.
  5. Hit ssthresh. The rule flips. Next growth is +1, not ×2. No packets move on this beat.
  6. RTT, CA. Send 8, ACK 8. Queue 0 — the pipe is exactly full. cwnd 8 → 9.
  7. RTT, CA. Send 9. Queue 1. cwnd 9 → 10.
  8. RTT, CA. Send 10. Queue 2. cwnd 10 → 11.
  9. RTT, CA. Send 11. Queue 3. cwnd 11 → 12.
  10. RTT, CA. Send 12. Queue 4/4. cwnd 12 → 13. The buffer is full; one more packet will not fit.
  11. Overflow. Send 13. Twelve fit, one drops. Three duplicate ACKs come back. cwnd stays 13 — the cut is the next beat.
  12. Cut. ssthresh = max(13 // 2, 2) = 6, cwnd = 6. State stays Congestion Avoidance. The chart point drops vertically; the ssthresh guide slides 8 → 6.
  13. RTT, CA. Send 6. cwnd 6 → 7. The first +1 after the cut, not a doubling. That is this lesson's simplified Reno recovery — we skip Fast Recovery's ssthresh + 3.
  14. RTT, CA. Send 7. cwnd 7 → 8.
  15. RTT, CA. Send 8. cwnd 8 → 9. Back at the bandwidth-delay product.
  16. Saw. The second tooth is drawn in one beat: 9 → 10 → 11 → 12 → 13, overflow, drop to 6. AIMD repeats.
  17. 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 ssthresh is a teaching choice. Real stacks often start ssthresh at the advertised window, so the first loss is what ends Slow Start. This lesson starts ssthresh = 8 so you see both rules before the first drop.
  • ssthresh never falls below 2. max(cwnd // 2, 2) keeps a degenerate window from collapsing to zero.
  • Slow Start can land exactly on ssthresh. Growing 4 with ssthresh = 8 produces 8, not 16. Growing 4 with ssthresh = 6 after a timeout clamps to 6, then CA takes over.
  • A timeout is not a triple-dupACK. Same cwnd = 13 overload; 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 cwnd while 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 ssthresh behaves. The actual predicate is cwnd < 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 ack growth as congestion control. The handshake's ack = seq + 1 is numbering. cwnd is 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.