The internet has no admission control: every TCP flow discovers for itself how fast it may send, by probing the network and reacting to signals. Congestion control is that discovery algorithm — arguably the most consequential control loop ever deployed — and its behavior explains a large share of real-world latency and throughput mysteries. This page covers the classic machinery, the modern algorithms, and the failure modes engineers actually meet.
Flow control protects the receiver: the advertised receive window caps in-flight data to what the receiver can buffer. Congestion control protects the network: the sender's congestion window (cwnd) caps in-flight data to what the path can carry. Effective sending rate ≈ min(cwnd, rwnd) / RTT — one formula that answers most "why is this transfer slow" questions: small window, long RTT, or both. The bandwidth-delay product (BDP) is the window needed to fill a path; high-BDP paths (transatlantic, high-bandwidth) need large windows and window scaling, and suffer most from anything that shrinks cwnd.
A new connection knows nothing, so it slow-starts: cwnd begins small (typically 10 segments) and doubles every RTT — exponential growth despite the name — until loss or a threshold. Then congestion avoidance takes over with AIMD: additive increase (one segment per RTT), multiplicative decrease (halve on loss). AIMD's genius is game-theoretic: it provably converges to fair sharing among competing flows without any coordination. The cost: sawtooth throughput, and a painful restart penalty — which is why connection reuse, keep-alives, and HTTP/2 multiplexing matter so much; every cold connection pays slow start again, and short web transfers often finish before ever leaving slow start, making RTT — not bandwidth — the dominant term for page loads.
Loss is inferred, not announced: three duplicate ACKs trigger fast retransmit (resend without waiting) and fast recovery (halve, keep going); SACK lets receivers report exactly which segments arrived, avoiding unnecessary resends. The disaster case is the retransmission timeout (RTO): no feedback at all forces a full timeout (≥ 200 ms–1 s), cwnd resets to one, and slow start begins again — tail-latency spikes in the hundreds of milliseconds are very often RTOs. On short flows, loss of the last packet cannot generate dupacks, so tail losses always cost an RTO (tail-loss probe exists precisely for this).
CUBIC (Linux default) refines loss-based AIMD: the window regrows along a cubic curve — fast to recover toward the previous level, cautious near it, then probing beyond — fairer at high BDP. Its philosophical limit is inherited: loss-based control fills buffers until they overflow, treating loss as the only congestion signal.
BBR (Google) changes the paradigm: continuously estimate the path's bottleneck bandwidth and minimum RTT, and pace sending at the estimated bandwidth while keeping in-flight data near the BDP — congestion control by model, not by loss. Results: dramatically better throughput on lossy paths (loss no longer read as congestion) and low queueing delay. Trade-offs are real: BBRv1 could bully CUBIC flows and stabilize with elevated queues in some regimes; v2/v3 add loss and ECN responsiveness. QUIC (HTTP/3) implements the same algorithms in user space, which is why transport innovation accelerated once it escaped kernel release cycles.
Oversized router buffers interact catastrophically with loss-based control: the sender keeps growing until the giant buffer finally overflows, so the buffer runs full — adding hundreds of milliseconds of standing queue delay. Symptom: ping times balloon whenever someone uploads. Fixes work at the queue: AQM (CoDel/FQ-CoDel, PIE) drops or ECN-marks early to signal before queues grow, and fq_codel's per-flow queuing isolates your video call from the bulk upload. ECN closes the loop without loss: routers mark instead of drop, receivers echo, senders back off. If you run latency-sensitive services or load tests, distinguishing bandwidth saturation from bufferbloat is a core diagnostic skill — the fix for one worsens the other.
RTT dominates short transfers; connection reuse is a performance feature, not hygiene; throughput ≈ window/RTT explains most single-flow ceilings; parallel connections are a blunt workaround that modern multiplexing replaces; and tail latency stories usually end at an RTO or a bloated buffer, not at average bandwidth.