Difficulty: Advanced
What is the difference between flow control and congestion control in TCP? Explain the sliding window, slow start, congestion avoidance and AIMD.
These two get confused constantly, so start with the cleanest distinction: flow control protects the receiver, congestion control protects the network. Flow control asks can the other end keep up with me. Congestion control asks can the path between us keep up with me. The sender obeys both, and the amount of unacknowledged data it may have in flight is the minimum of the two limits.
Flow control uses the sliding window. In every ACK the receiver advertises a receive window (rwnd): the amount of free buffer space it has. The sender may send up to that many bytes beyond the last acknowledged byte without waiting. As the application reads data and buffer space frees up, the window slides forward. If the receiver's application is slow and the buffer fills, rwnd drops to zero, and the sender stops and periodically sends tiny zero-window probes until space opens. The window scale option extends the 16-bit field to allow windows of up to about 1 GB, essential for high bandwidth links. Silly window syndrome, where tiny windows cause tiny inefficient segments, is avoided by the receiver not advertising small windows and the sender (Nagle's algorithm) coalescing small writes.
Congestion control deals with the network in the middle, which does not tell TCP anything directly. TCP infers congestion from packet loss (or, in modern variants, from rising delay or ECN marks) and maintains a second limit, the congestion window (cwnd). The sender's effective window is min(rwnd, cwnd).
It works in phases. Slow start: the connection starts with a small cwnd (initial window of 10 segments on modern Linux, older was 1 to 4). Despite the name, growth is exponential: for every ACK cwnd increases by one segment, so it roughly doubles every round trip, 10, 20, 40, 80. This continues until cwnd reaches the slow start threshold, ssthresh, or a loss happens. Congestion avoidance: above ssthresh, growth becomes linear, roughly one segment per round trip. Reaching this is called additive increase.
On loss, TCP backs off multiplicatively. If loss is detected by a timeout (severe), ssthresh is set to half the current cwnd and cwnd resets to 1 (or the initial window), returning to slow start. If loss is detected by three duplicate ACKs (milder, because packets are still flowing), TCP Reno does fast recovery: ssthresh becomes cwnd/2 and cwnd is set to ssthresh, continuing in linear growth. This overall pattern is AIMD, Additive Increase Multiplicative Decrease, which produces the sawtooth graph of cwnd over time. AIMD is provably fair and stable when many flows share a bottleneck, since the larger flow backs off by more in absolute terms.
Extras that show depth: TCP CUBIC is the default on Linux, using a cubic function to regain bandwidth quickly on fast long links; BBR from Google models bottleneck bandwidth and round trip time instead of using loss as the signal, and does well on lossy links; ECN lets routers mark packets rather than drop them. Remember also that UDP has neither, so a UDP-heavy application must implement its own congestion behaviour or it can flood the network, which QUIC does. For calculations, throughput is roughly window size divided by RTT, so a 64 KB window on a 100 ms link caps at about 5 Mbps regardless of link capacity.
ssthresh = 16, cwnd starts at 1
RTT 1: cwnd=1 (slow start)
RTT 2: cwnd=2
RTT 3: cwnd=4
RTT 4: cwnd=8
RTT 5: cwnd=16 (reach ssthresh)
RTT 6: cwnd=17 (congestion avoidance, +1 per RTT)
RTT 7: cwnd=18
-- 3 duplicate ACKs at cwnd=18 --
ssthresh = 9, cwnd = 9 (fast recovery), then +1 per RTT
-- timeout at cwnd=12 --
ssthresh = 6, cwnd = 1, back to slow start
Exponential below ssthresh, linear above, halve on loss. That is the sawtooth.
Sliding Window, Flow Control, Congestion Control, Slow Start, AIMD