Skip to content
academia.sh

Lesson 16 / 18

Congestion Control

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

Contents

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:

effective window=min(cwnd, rwnd)\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.

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.

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:

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

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 MSS/RTT3/2p\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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close