Congestion control
Flow control solved a problem entirely local to the two endpoints of a connection: how much can this specific receiver's buffer hold. Congestion control solves a harder problem, because the thing it's protecting isn't owned by either endpoint at all. Between a sender and a receiver sit routers and switches, each with its own finite buffer, each shared at that exact moment by however many other connections happen to be passing through it. TCP has no direct visibility into any of that hardware — no router sends a live "my buffer is at 80%" status update — so congestion control is fundamentally a problem of inference: figuring out how much the network in between can absorb, using only signals the sender can observe from its own side of the connection.
The buffer nobody at either end can see
Picture a router forwarding packets from dozens of different TCP connections onto a single outbound link. If those connections collectively try to send more data than the link can carry, the excess has to go somewhere, and "somewhere" is the router's own buffer — a queue of packets waiting their turn. That buffer is finite. If it fills faster than it drains, the router runs out of space and starts dropping packets, with no warning to either the sender or the receiver beyond the fact that data simply doesn't arrive.
This is a genuinely different situation from the one flow control addresses. A slow receiver announces its limit explicitly, in every segment, as a number the sender can read directly. A congested router does nothing of the kind by default — it just silently discards packets once its buffer is full, and TCP's only way to notice is that expected acknowledgments stop arriving. Congestion control is the discipline built around inferring that silence correctly and reacting to it before it gets worse.
The congestion window: the sender's own estimate
TCP tracks this as the congestion window (CWND), maintained entirely on the sender's side — unlike the receive window, CWND is never transmitted in a header field, because there's no single authority on the other end to report it. It represents the sender's current best guess at how much unacknowledged data the network path can carry without inducing loss, and as Sliding window already noted, the sender always uses whichever is smaller at any given moment: the receiver's advertised window, or its own congestion window.
That congestion window isn't fixed. It starts deliberately small, grows as the connection proves the network path can handle more, and shrinks sharply the moment loss suggests it grew too far. Exactly how it grows and shrinks is a whole family of algorithms in its own right — Slow start, TCP Reno, TCP CUBIC, and TCP BBR each answer that differently, and are covered in the articles that follow this one. What this article covers is the shared groundwork underneath all of them: how loss is detected in the first place, and what a router can do besides simply dropping packets.
Detecting loss: two different signals
A TCP sender has two independent ways to conclude that a segment was probably lost, and they lead to different responses:
- Retransmission timeout (RTO). Every unacknowledged segment has a timer, calculated from the connection's observed round-trip time. If that timer expires with no acknowledgment, TCP assumes the segment (or its ACK) was lost, and — critically — treats this as a strong signal that the network is significantly congested, since timeouts generally only happen after a real, sustained delay. This triggers the harshest response available: the congestion window collapses back down to its starting value, and the connection effectively restarts its ramp-up from scratch.
- Duplicate ACKs. Because TCP's acknowledgments are cumulative (as Sliding window explained), a receiver that gets segments out of order — segment 4 arriving before the still-missing segment 3 — keeps re-sending an ACK for the last segment it received in full sequence. Several duplicate ACKs in a row is the sender's signal that a specific segment, not the whole connection, has probably gone missing, and it's a much weaker congestion signal than a full timeout: the fact that later segments did arrive means the path is still moving data, just missing one piece. This is what triggers fast retransmit — resending the specific missing segment immediately, without waiting for its timer to expire — and the more measured congestion-window reduction that TCP Reno implements in response.
The distinction matters in practice: a connection experiencing occasional out-of-order delivery on an otherwise healthy path recovers quickly through duplicate ACKs and fast retransmit, barely denting its congestion window. A connection hitting a full timeout pays a much bigger, slower penalty — which is exactly why the different congestion-control algorithms covered next spend real design effort trying to detect and recover from loss via duplicate ACKs whenever possible, reserving the timeout path for cases the faster mechanism genuinely can't catch.
Letting routers warn before they drop: ECN
The signals above both depend on loss having already happened. Explicit Congestion Notification (ECN), an optional extension both endpoints and the routers in between must support, offers a way to react before that point: a router whose buffer is filling up, but not yet full, can mark a bit in the IP header of a passing packet instead of dropping it outright. The receiver copies that marking back to the sender in its next TCP acknowledgment, and a sender that sees it responds exactly as if a loss event had occurred — reducing its congestion window — without any data actually having been dropped or retransmitted.
ECN requires cooperation from every hop along the path, not just the two endpoints, which is part of why it isn't universally deployed even though the endpoints alone can't force it. Where it is supported end to end, it turns congestion signaling from "silence, then infer" into an explicit, low-cost warning — a router gets to say "getting close" instead of the network's only tool being to simply start discarding traffic.
Watching a congestion event in a capture
A real congestion event is visible as a distinctive pattern: steady acknowledgments, then a gap, then either a retransmission or a run of duplicate ACKs.
15:02:01.100010 IP 10.0.5.12.51900 > 93.184.216.34.443: Flags [P.], seq 41601:43001, ack 900, win 2053, length 1400
15:02:01.140220 IP 93.184.216.34.443 > 10.0.5.12.51900: Flags [.], ack 43001, win 1026, length 0
15:02:01.201880 IP 10.0.5.12.51900 > 93.184.216.34.443: Flags [P.], seq 43001:44401, ack 900, win 2053, length 1400
15:02:01.560310 IP 93.184.216.34.443 > 10.0.5.12.51900: Flags [.], ack 43001, win 1026, length 0
15:02:01.561100 IP 93.184.216.34.443 > 10.0.5.12.51900: Flags [.], ack 43001, win 1026, length 0
15:02:01.561840 IP 93.184.216.34.443 > 10.0.5.12.51900: Flags [.], ack 43001, win 1026, length 0
The last three lines are all acknowledging the same sequence number (ack 43001) even though the client sent a further segment afterward — three duplicate ACKs, matching this article's description exactly, telling the sender that the segment starting at 43001 hasn't arrived, while everything up to it has. A sender's TCP stack seeing this pattern triggers fast retransmit for that specific segment well before its retransmission timer would otherwise have expired.
Practical scenario: throughput that collapses on a shared uplink, not a broken link
An office's internet connection tests fine for a single machine running a speed test — full advertised bandwidth, low latency — but during the workday, with dozens of employees online simultaneously, everyone's connections feel sluggish rather than outright broken: pages load slowly, video calls degrade in quality, nothing fails outright. There's no reported outage from the ISP, and a ping to the gateway shows normal, low latency throughout.
A packet capture on a busy workstation during this period shows occasional runs of duplicate ACKs and the occasional retransmitted segment, on multiple unrelated connections at once — not a single misbehaving application, but a pattern spread across simultaneous, otherwise-unrelated traffic. That's consistent with congestion on the shared uplink itself, not with any one connection or any one router being individually broken: the office's outbound link has a fixed capacity, every employee's TCP connections are contending for it during peak hours, and the router at the edge of the office network is doing exactly what routers do when a shared link is oversubscribed — buffering what it can and dropping the rest, which every affected connection then perceives as loss and reacts to by shrinking its own congestion window.
The plain ping test staying fast throughout is not a contradiction — a small ICMP echo request is easy for a router to prioritize and pass through even when its buffer for bulk TCP traffic is under real pressure, which is exactly why "ping looks fine" is not proof that a link isn't congested for actual data transfer. The fix here isn't a network setting to tune on any single machine; it's either more upstream bandwidth, or traffic shaping at the router to prioritize latency-sensitive traffic (video calls, voice) over bulk transfers (large downloads, backups) during contention — a topic this course returns to later when it covers traffic management in depth.
Practice exercises
- Explain, using this article's distinction between the two loss-detection signals, why a sender that receives three duplicate ACKs typically recovers faster and with a smaller performance hit than one whose retransmission timer expires — for the same single lost segment.
- A colleague argues that ECN makes retransmission timeouts unnecessary. Explain why that's not correct, using the requirement that ECN depends on for it to work at all.
- In the shared-uplink scenario above, explain why testing from a single idle machine outside peak hours would show a healthy connection even though the same office's network is genuinely congested during the day — what does that single-machine test fail to reproduce?
Detecting congestion and reacting to it is the shared foundation; the interesting differences between TCP implementations are in exactly how the congestion window grows when things are going well and shrinks when they aren't. Slow start covers the very beginning of that story — how a brand-new connection decides it's safe to speed up in the first place.