Introduction

Every time you stream a video, send an email, or open a webpage, your computer is silently running one of the most consequential algorithms in engineering history.

The problem is brutal: thousands of TCP flows share the same physical wires, but no central authority tells them how fast to go. Send too slowly and you waste capacity. Send too fast and routers drop packets, turning a mild slowdown into congestion collapse — the state the early Internet nearly reached in 1986, when throughput on some links fell by a factor of 1,000.

The solution, invented by Van Jacobson in 1988 and refined over decades, is a simple two-rule loop:

  1. Additive Increase (AI): every round-trip time that passes without a loss, grow the congestion window by one packet. Probe for more bandwidth.
  2. Multiplicative Decrease (MD): the moment a packet is lost, halve the window. Back off sharply.

This AIMD rhythm produces the famous sawtooth shape of the congestion window over time — the visual signature of every TCP connection alive today.

Watch the Sawtooth

The chart below simulates TCP Reno's AIMD loop. The congestion window (cwnd) starts small, grows quickly during slow start, then climbs linearly — until a packet loss event cuts it in half. Hit Run to watch the cycle repeat.

<!-- {{c_demo_title}} -->
<div class="controls">
  <label>{{lbl_loss}} <input type="range" id="lossSlider" min="1" max="30" value="8"> <span id="lossVal">8%</span></label>
  <label>{{lbl_speed}} <input type="range" id="speedSlider" min="1" max="10" value="5"> <span id="speedVal">5</span></label>
</div>
<canvas id="chart" width="560" height="220" aria-label="{{canvas_aria}}"></canvas>
<div class="stats">
  <span id="statCwnd">{{lbl_cwnd}}: 1</span>
  <span id="statThroughput">{{lbl_avg}}: —</span>
  <span id="statPhase">{{phase_slow_start}}</span>
</div>
<div class="btns">
  <button id="btnRun" type="button">{{btn_run}}</button>
  <button id="btnStep" type="button">{{btn_step}}</button>
  <button id="btnReset" type="button" class="ghost">{{btn_reset}}</button>
</div>
<div class="legend">
  <span class="dot blue"></span>{{legend_cwnd}}
  <span class="dot red"></span>{{legend_ssthresh}}
  <span class="dot orange"></span>{{legend_loss}}
</div>
/* {{c_css_base}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .5rem; font-size: .85rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #d0d7de; border-radius: 8px; background: #fafbfc; }
.stats { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin: .4rem 0; font-size: .85rem; font-weight: 600; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { display: flex; align-items: center; gap: .5rem; font-size: .78rem; color: #555; flex-wrap: wrap; margin-top: .3rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 2px; }
.dot.blue { background: #1d76c8; }
.dot.red { background: #c92f3c; }
.dot.orange { background: #e07b00; }
// Code not found

Notice that the window never stays constant: it probes upward, detects a loss signal, retreats, and probes again. Adjust the loss probability slider to make drops rarer or more frequent and watch how the average throughput changes. The sawtooth is not a bug — it is the algorithm doing exactly what it was designed to do.

The Real Complexity

AIMD looks deceptively simple, but its theoretical properties are surprisingly deep.

The throughput formula. Under steady-state AIMD with random losses, the average throughput of a single TCP flow is approximately:

TCRTTpT \approx \frac{C}{\text{RTT} \cdot \sqrt{p}}

where CC is a small constant, RTT\text{RTT} is the round-trip time, and pp is the loss probability. Two consequences jump out:

  • RTT unfairness: flows with a shorter round-trip time probe faster and therefore grab more bandwidth, even on the same bottleneck link.
  • High-bandwidth sensitivity: as pp falls, throughput grows only as 1/p1/\sqrt{p} — to double throughput you need to quarter the loss rate.

Slow start. Rather than starting at window size 1 and increasing linearly, TCP Tahoe/Reno begin in slow start: the window doubles every RTT until it hits the slow-start threshold (ssthresh\text{ssthresh}), then switches to additive increase. This gets a cold flow to a useful rate quickly without monopolising the link.

Bufferbloat. The formula above assumes that packet loss is the signal. But modern networks often have huge buffers that absorb bursts without dropping anything — so TCP never sees a loss and inflates the window until latency explodes. This bufferbloat problem drove the development of Active Queue Management schemes (RED, CoDel, FQ-CoDel) that deliberately signal congestion before buffers fill.

Modern variants. TCP Cubic (the default in Linux since 2006) replaces the linear increase with a cubic function of time since the last loss, achieving much higher throughput on long, fat pipes. Google's BBR ignores loss entirely and models the bottleneck bandwidth directly, using delay and delivery rate as its signal. Both can be understood as responses to the limits of pure AIMD revealed by the formula above.

Where It Matters

AIMD and its descendants are the invisible hand that governs almost every byte on the Internet:

  • Web browsing (HTTP/1.1, HTTP/2): every TCP connection fetching a webpage runs a congestion-control algorithm. The initial slow-start phase is why the first bytes of a page sometimes feel slow even on a fast connection.
  • QUIC and HTTP/3: Google's QUIC protocol implements congestion control in userspace (BBR by default), escaping the kernel's TCP stack for faster iteration. It now carries a large fraction of YouTube and Google Search traffic.
  • Video streaming: adaptive bitrate players (Netflix, YouTube, Twitch) sit on top of TCP or QUIC and exploit the throughput signal to choose the right quality tier. The congestion window is the bandwidth estimate.
  • Data-centre fabrics: inside a warehouse-scale computer, RDMA and custom transports such as DCQCN replace TCP but implement the same AIMD principle — explicit congestion notification (ECN) marks packets instead of dropping them, enabling sub-millisecond reaction times.
  • Satellite and long-distance links: high-latency paths expose RTT unfairness brutally. Protocols like HSTCP, SCTP and PEPs (Performance-Enhancing Proxies) were all born from the need to overcome the 1/(RTTp)1/(\text{RTT} \cdot \sqrt{p}) ceiling.

Even the halting problem and P vs NP feel abstract compared to the stakes here: a wrong congestion-control decision collapses real networks in real time.

Conclusion

TCP congestion control is a masterpiece of distributed systems design. No central controller allocates bandwidth; no global state is shared. Each sender follows two local rules — increase a little, decrease a lot — and the emergent behaviour is a network that is roughly fair, roughly efficient, and self-healing after congestion.

The sawtooth is the algorithm's signature: every dip is a sender backing off, every climb is a sender probing again. Van Jacobson's insight saved the Internet from collapse in 1988 and, in its many descendants, still governs the flow of data across the globe today.

If you want to go deeper, the same P vs NP tension between easy verification and hard optimisation appears when you ask whether there is a provably optimal congestion-control policy for a given network — a question that remains open.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/tcp-congestion-control/Content licensed under CC BY-NC 4.0.