---
title: "Congestion Control"
description: "Textbook Reno: slow start, AIMD, timeout"
source: https://vizlearn.app/cs/networks/congestion-control
access: premium (interactive player requires Lifetime; this doc is free)
---

# 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`.
