---
title: 'Congestion Control'
source: 'https://academia.sh/en/courses/network-models/congestion-control'
course: 'Network Models and Protocols'
language: en
updated: '2026-08-17T18:07:11+00:00'
license: 'CC BY-SA 4.0'
---

# Congestion Control

Congestion window, slow start and additive increase-multiplicative decrease, reading loss as a signal, and the relationship between loss rate and throughput.

The previous lesson's flow control protects the receiver's capacity. If the receiver
announces a one-megabyte window, the sender earns the right to send that amount without
acknowledgement — without accounting for what happens along the way.

The path may not be able to carry that much data. Router queues fill up, packets are
dropped, dropped packets are retransmitted, and retransmissions fill the queues even
further. This lesson's question is how the sender can estimate a capacity it cannot see.

## Congestion

**Congestion** is traffic arriving at a link exceeding that link's capacity. The router
queues the excess packets; once the queue fills, it starts dropping them.

Left uncontrolled, a feedback loop forms. Dropped packets are retransmitted, retransmissions
increase the load, and as the load increases, more packets are dropped. As a result, the
amount of **useful** data crossing the link falls even as load rises. This is called
**congestion collapse**.

The difficulty of the problem is lack of information. The sender knows neither the path's
capacity nor who else shares that path. The only information it has is what happens to the
segments it sends itself. For this reason, congestion control is a **measure-and-adapt**
loop: the sender increases its rate, observes the network's response, and adjusts based on
the response.

## Congestion Window

In addition to the receive window, the sender keeps a second limit: the **congestion window
(cwnd)**. This value does not come from the other side; it is the sender's own estimate.

The amount of data that can be sent without acknowledgement is the smaller of the two:

$$
\text{effective window} = \min(\text{cwnd},\ \text{rwnd})
$$

The distinction matters. The receive window protects the **receiver**, the congestion
window protects the **network**. If a transfer is slow, the question to ask is which one is
limiting it: if the receiver's buffer is small, the receive window is the bottleneck; if the
path is congested, the congestion window is.

## Slow Start

When a connection is established, there is no information about the path's capacity. The
initial value is chosen small and is increased with **every acknowledgement**. Since a
window's worth of segments gets acknowledged over one round, the window doubles every
round.

```python
MSS = 1460

cwnd = 1
for rnd in range(1, 12):
    print(f"round {rnd:2d}: cwnd={cwnd:5d} MSS = {cwnd * MSS / 1024:8.1f} KiB")
    cwnd *= 2
```

```
round  1: cwnd=    1 MSS =      1.4 KiB
round  2: cwnd=    2 MSS =      2.9 KiB
round  3: cwnd=    4 MSS =      5.7 KiB
round  4: cwnd=    8 MSS =     11.4 KiB
round  5: cwnd=   16 MSS =     22.8 KiB
round  6: cwnd=   32 MSS =     45.6 KiB
round  7: cwnd=   64 MSS =     91.2 KiB
round  8: cwnd=  128 MSS =    182.5 KiB
round  9: cwnd=  256 MSS =    365.0 KiB
round 10: cwnd=  512 MSS =    730.0 KiB
round 11: cwnd= 1024 MSS =   1460.0 KiB
```

The phase's name is misleading: the growth is **exponential** and fast. The word "slow"
describes starting from a small value rather than jumping directly to capacity.

Exponential growth continues up to a threshold. The phase changes when the **slow start
threshold** is exceeded or a loss is detected.

## Additive Increase, Multiplicative Decrease

The phase after the threshold is **congestion avoidance**, and the growth is linear: one
MSS is added to the window every round.

When loss is detected, the window is reduced **multiplicatively** — the common behavior is
halving it. The two rules together are called **additive increase, multiplicative decrease
(AIMD)**.

The asymmetry is deliberate. Because the increase is slow, capacity overshoot stays bounded;
because the decrease is fast, congestion eases quickly. Flows sharing the same path balance
their shares under this rule: a flow taking more than its share is affected more by loss and
its share drops.

The cost of this asymmetry shows up on high-capacity, high-latency paths.

```python
import math

MSS = 1460
bdp = 1000e6 * 0.100 / 8
target = bdp / MSS
print(f"bandwidth-delay product: {bdp/1e6:.1f} MB = {target:.0f} MSS")
rounds_exp = math.ceil(math.log2(target))
print(f"with exponential growth: {rounds_exp} rounds = {rounds_exp * 100} ms")
rounds_lin = round(target / 2)
print(f"with linear growth (from half): {rounds_lin} rounds = {rounds_lin * 100 / 1000:.1f} s")
```

```
bandwidth-delay product: 12.5 MB = 8562 MSS
with exponential growth: 14 rounds = 1400 ms
with linear growth (from half): 4281 rounds = 428.1 s
```

On a path with 1 Gb/s capacity and 100 ms latency, the window reaching capacity in the
exponential phase takes 1.4 seconds. If a single loss halves the window, climbing back with
linear growth takes more than seven minutes. On paths at this scale, classic AIMD behavior
cannot use the capacity; this is the rationale for the approaches covered in the next
section.

## Reading Loss as a Signal

Classic congestion control treats packet loss as an indicator of congestion. The assumption
is this: on a wired path, bit errors are rare, so the cause of loss is most likely a full
queue.

There are cases where the assumption does not hold. On wireless links, packet loss can stem
from interference or a weak signal; in that case halving the window does not solve the
problem, it only lowers throughput. Likewise, reordering caused by a temporary path change
produces duplicate acknowledgements and can trigger an unnecessary reduction.

There is a way to signal congestion without waiting for loss to occur: **explicit
congestion notification**. A router whose queue starts filling marks two bits in the IP
header instead of dropping the packet; the receiver reports this back in its
acknowledgement, and the sender lowers its rate without losing a packet. For the method to
work, the nodes along the path and both ends must support it.

A third approach is to read the **increase in delay** as a signal without waiting for loss
at all: round-trip time rises as queues start filling. Delay-based methods keep queues empty
and keep delay low; but when they share a path with loss-based flows, they lose share,
because the others do not back off until the queue fills.

## Fast Retransmit and Fast Recovery

Loss is detected in two ways, and the response differs.

| Detection path | Meaning | Response |
|---|---|---|
| Timeout | No word from the network at all | Window drops to its smallest value, returns to slow start |
| Three duplicate acknowledgements | Later segments are arriving | Window halves, congestion avoidance continues |

The reason for the distinction is the amount of information available. If duplicate
acknowledgements are arriving, the path has not broken entirely: later segments are reaching
the receiver. In that case starting from zero is unnecessary; the window is halved and the
flow continues. This is called **fast recovery**.

On a timeout, however, no acknowledgement has arrived at all; the state of the path is
unknown and it must be measured from scratch.

## Loss Rate and Throughput

The long-term throughput of a loss-based flow depends on the loss rate and the round-trip
time. Averaging the sawtooth-shaped window behavior yields the following approximation:

$$
\text{throughput} \approx \frac{\text{MSS}}{\text{RTT}} \cdot \sqrt{\frac{3}{2p}}
$$

```python
import math

MSS = 1460
for p in (1e-2, 1e-3, 1e-4, 1e-5, 1e-6):
    v = MSS * math.sqrt(1.5) / (0.100 * math.sqrt(p))
    print(f"p={p:.0e} -> {v * 8 / 1e6:8.2f} Mb/s")
```

```
p=1e-02 ->     1.43 Mb/s
p=1e-03 ->     4.52 Mb/s
p=1e-04 ->    14.31 Mb/s
p=1e-05 ->    45.24 Mb/s
p=1e-06 ->   143.05 Mb/s
```

Two results can be read off. First, throughput is inversely proportional to the **square
root** of the loss rate: reducing loss a hundredfold increases throughput only tenfold.
Second, throughput is inversely proportional to round-trip time — a distant server yields
lower throughput at the same loss rate.

This relationship also explains why, of two flows sharing the same path, the one with lower
latency takes a larger share. A short round-trip time gives more frequent opportunities to
grow its window.

The formula is an approximation and has limits: it is valid only for loss-based behavior, in
long-lived and stable flows. Short transfers most often do not even finish the slow start
phase.

## Bufferbloat

Keeping router buffers large reduces packet loss but increases queuing delay. If the
control algorithm is waiting for loss, it keeps increasing its rate until the buffer fills;
the result is reduced loss and increased delay. This is called **bufferbloat**.

The symptom is familiar: while a large transfer is underway, the response time of
interactive use on the same link noticeably lengthens. The cause is not insufficient
capacity but queued packets waiting behind a full queue.

The countermeasure is not shrinking the buffer but managing the queue **actively**: dropping
or marking packets early once queue wait exceeds a threshold. This way the sender gets the
signal before the queue is completely full.

## Fairness

How flows sharing the same bottleneck divide up capacity is a separate metric. AIMD
converges toward balance among flows with equal round-trip times. If the times differ, the
balance breaks down; the flow with the shorter time gets a larger share.

An application opening many concurrent connections also increases its share: each
connection runs its own AIMD cycle. This is the simplest way an application can get around
congestion control, and it is one of the design rationales for application protocols that
multiplex over a single connection.

## Summary

- Congestion is traffic arriving at a link exceeding its capacity; left uncontrolled,
  retransmissions raise the load and lead to collapse.
- The sender keeps two windows; the amount that can be sent without acknowledgement is the
  smaller of the congestion and receive windows.
- In slow start the window doubles every round; after the threshold it grows by one MSS per
  round and shrinks multiplicatively on loss.
- On a 1 Gb/s, 100 ms path, capacity is reached in 1.4 seconds in the exponential phase;
  climbing back from half in the linear phase takes 428 seconds.
- Loss-based throughput is approximated by $\text{MSS}/\text{RTT} \cdot \sqrt{3/2p}$;
  throughput is inversely proportional to the square root of the loss rate and to
  round-trip time.
- A timeout returns the flow to slow start; three duplicate acknowledgements only halve the
  window; large buffers reduce loss while increasing delay.

## Next Step

The guarantees TCP offers are not free: the handshake costs one round, retransmission on
loss costs extra rounds, and ordered delivery brings waiting. Not every application needs to
pay this cost. The next lesson covers the transport protocol that offers no guarantees at
all, and which applications this simplicity is the right choice for.
