← All chaptersChapter 86 sections

🚦 Congestion Control

How senders share a finite network without collapsing it: congestion collapse and the AIMD feedback loop, slow start and congestion avoidance, loss-based recovery, CUBIC for high-BDP paths, BBR and delay-based control, and fairness, AQM, ECN and datacenter congestion.

8.1 Why congestion control exists

Congestion is a shared-resource failure. A packet network is built from links, switches, routers, NIC queues, DMA rings, software queues, and scheduler decisions. Every one of those places has finite service rate and finite buffering. If the senders feeding a bottleneck collectively inject packets faster than the bottleneck can transmit them, the excess waits in a queue or is dropped. For a short burst, buffering is useful. For sustained overload, buffering just delays the visible failure. The queue grows, RTT rises, and eventually packets are discarded.

Many flows meet at one bottleneck queue; unchecked, it overflows and drops.
Many flows meet at one bottleneck queue; unchecked, it overflows and drops.

That sounds ordinary until retransmission enters the system. A reliable transport interprets missing data as work to do again. If many hosts keep transmitting new data while also retransmitting data that was already dropped, the network spends more of its finite capacity carrying packets that will not advance any application byte stream. Offered load rises, but useful throughput falls. This is congestion collapse: links are busy, interrupt paths are hot, queues are full, but delivered goodput drops because the control loop is wrong.

Congestion collapse: past the knee, more load means less useful throughput.
Congestion collapse: past the knee, more load means less useful throughput.

The early Internet hit this problem in practice. Van Jacobson and Michael Karels opened their 1988 SIGCOMM paper by describing the October 1986 Internet congestion collapses, where throughput between Lawrence Berkeley Laboratory and UC Berkeley fell dramatically despite the underlying links still existing. The lesson was not that TCP needed a bigger retransmission timer, or that routers needed infinite memory. The lesson was that end hosts had to stop treating the network as an invisible pipe. A sender must estimate how much data the path can absorb and must reduce its sending rate when the path shows signs of overload.

This is easy to say and hard to do because the Internet has no central scheduler for ordinary flows. A TCP sender usually does not know the bottleneck link rate, the number of competing flows, the queue depth in a router, whether the bottleneck is a WAN link, a top-of-rack uplink, a Wi-Fi hop, a policer, or a host receive path. It sees only indirect feedback: ACK timing, missing ACKs, duplicate ACKs, retransmission timeouts, sometimes ECN marks, and local signals such as pacing state and socket buffer pressure. Congestion control is the discipline of turning those weak signals into a stable distributed feedback loop.

Congestion control is a closed feedback loop with no central controller.
Congestion control is a closed feedback loop with no central controller.

The first distinction to keep clean is flow control versus congestion control. Flow control protects the receiver. The receiver advertises rwnd, the receive window, saying how many more bytes it can accept without overflowing its socket buffer. If an application stops reading, rwnd shrinks, possibly to zero, even if the network is empty. Congestion control protects the network path. The sender maintains cwnd, the congestion window, its estimate of how much unacknowledged data may safely be in flight. RFC 5681 states the operational rule: the sender must not send beyond the highest acknowledged sequence number plus the minimum of cwnd and rwnd. In shorthand, bytes in flight are limited by min(rwnd, cwnd).

The sender is limited by the smaller of the receive window and the congestion window.
The sender is limited by the smaller of the receive window and the congestion window.

That min matters in low-level work. A receive-window problem is often a host/application problem: slow userspace, small socket buffers, memory pressure, copy overhead, delayed wakeups, or poor receive-side scaling. A congestion-window problem is often a path problem: loss, ECN marks, queue growth, ACK compression, pacing interactions, or competition at a bottleneck. A full RX ring can cause drops that look like network loss to the peer. Large TSO/GSO bursts can dump more bytes into a qdisc or device queue than the pacing model intended. Interrupt moderation can reshape ACK timing.

Symptom triage: which window is the limiter routes you to the host side or the path side.
Symptom triage: which window is the limiter routes you to the host side or the path side.

The core goal is not merely β€œavoid drops.” A network with zero drops can still be congested if bottleneck queues stay full and every flow pays inflated latency. Nor is the goal β€œmaximize one sender.” A shared network must allocate capacity among flows with different RTTs, packet sizes, start times, and implementations. TCP congestion control gives cooperative transports a behavioral contract: probe for capacity, infer overload, and yield enough that other flows can make progress.

A full standing queue inflates latency for every flow even when nothing is dropped.
A full standing queue inflates latency for every flow even when nothing is dropped.

The classic intuition is AIMD: additive increase, multiplicative decrease. Additive increase means probing upward gently. If ACKs keep returning and no congestion signal appears, the sender increases its window by a small amount over time. Multiplicative decrease means backing off hard when congestion is inferred. A loss event is not treated as a request to shave off one packet; it is treated as evidence that the current aggregate offered load exceeded the bottleneck’s sustainable capacity. Cutting the window by a fraction quickly drains pressure from the queue and gives competing flows room.

AIMD: probe upward additively, then cut hard on a congestion signal.
AIMD: probe upward additively, then cut hard on a congestion signal.

AIMD has a fairness story as well as a stability story. Imagine two long-lived flows sharing one bottleneck. If both increase linearly, their combined load gradually approaches the bottleneck capacity. When congestion occurs, multiplicative decrease pulls both back in proportion to their current rates. Repeating that cycle tends to move cooperative flows toward a fair share while still keeping the link busy. RTT, ACK behavior, queue management, offloads, and the chosen algorithm all affect the exact result, but the shape of the argument is why β€œprobe up gently, back off hard” became the baseline design.

Two flows on a fairness diagram: AIMD walks the operating point toward the equal-share line.
Two flows on a fairness diagram: AIMD walks the operating point toward the equal-share line.

This also explains why congestion control belongs at the endpoints even though congestion happens inside the network. Routers and switches can help by dropping, marking with ECN, scheduling fairly, or running active queue management. Datacenter fabrics can add stronger signals because the operator controls more of the environment. But the sender is where application demand becomes packets. The sender chooses how many bytes are in flight, how bursts are paced, how retransmissions are scheduled, and how quickly a flow recovers after a signal.

The rest of the chapter follows from this feedback-loop view. Slow start asks how a new flow can find capacity without beginning at line rate. Congestion avoidance and AIMD describe the steady-state sawtooth that made classic TCP safe for the shared Internet. Fast retransmit and recovery respond when loss is visible before a timeout. CUBIC changes the probing function for high-bandwidth, high-delay paths. BBR asks whether loss is the wrong primary signal and instead models bottleneck bandwidth and propagation delay. AQM, ECN, DCTCP, and datacenter congestion control tighten the loop where microseconds, shallow buffers, incast, and NIC offloads make old assumptions visible.

The invariant underneath all of them is simple: a sender must not confuse its ability to generate packets with the network’s ability to deliver useful bytes. Congestion control exists because that difference is real, shared, dynamic, and destructive when ignored.

Sources

8.2 Slow start, congestion avoidance, AIMD

Classic TCP congestion control is a sender-side control loop around one variable: the congestion window, cwnd. The receiver window says how much buffer space the peer has advertised; cwnd says how much unacknowledged data the sender believes the network can safely hold. The sender may have at most roughly min(cwnd, rwnd) bytes in flight. The algorithm is about how quickly cwnd should grow, and how sharply it should fall, with only ACK timing and loss as feedback.

Bytes in flight are capped by min(cwnd, rwnd); ACKs and loss are the only feedback.
Bytes in flight are capped by min(cwnd, rwnd); ACKs and loss are the only feedback.

The first problem is startup. A new connection usually has no reliable estimate of the path's bandwidth-delay product. Sending one segment per RTT forever would be safe but wasteful; blasting a full socket buffer into an unknown path can overflow a queue. Slow start is the compromise. Despite the name, it is the aggressive phase: start with an initial window, IW, and increase cwnd by at most one sender maximum segment size, SMSS, for each ACK that cumulatively acknowledges new data. If ACKs arrive for a window's worth of data, the next RTT can send about twice as much as the previous RTT.

Slow start: cwnd doubles every RTT until it reaches ssthresh.
Slow start: cwnd doubles every RTT until it reaches ssthresh.

That per-ACK rule matters more than the cartoon. With one ACK per segment, cwnd goes from 1, to 2, to 4, to 8 segments over successive RTTs. With delayed ACKs, ACK thinning, GRO/LRO effects, or application-limited sending, growth is not a perfect power of two. Modern TCP also commonly starts above one segment; RFC 6928 specified an experimental permitted initial window of 10 segments, with fallback guidance. The shape is unchanged: exponential probing, driven by the returned ACK clock, until the sender has evidence that the path is near capacity.

Per-ACK growth: one ACK per segment doubles cwnd; thinning and IW=10 bend the curve.
Per-ACK growth: one ACK per segment doubles cwnd; thinning and IW=10 bend the curve.

The boundary between the two growth modes is ssthresh, the slow-start threshold. When cwnd < ssthresh, the sender uses slow start. When cwnd > ssthresh, it uses congestion avoidance. At equality, RFC 5681 permits either behavior. After a loss event, ssthresh is set from the current flight size, usually to about half of the outstanding data, subject to a minimum of 2 * SMSS. From then on, ssthresh records a rough memory of where the previous attempt became too aggressive.

The cwnd controller: slow start, congestion avoidance, fast recovery.
The cwnd controller: slow start, congestion avoidance, fast recovery.

Congestion avoidance is the gentler phase. Instead of doubling each RTT, classic TCP increases cwnd by about one SMSS per RTT. Implementations spread that increase across ACKs: each ACK contributes a small fraction, so a whole window of ACKs adds up to about one segment. This is additive increase. It is slow because the sender has found a plausible operating point and is probing for spare capacity without causing large queue swings.

Congestion avoidance: +1 MSS per RTT β€” gentle linear probing.
Congestion avoidance: +1 MSS per RTT β€” gentle linear probing.

A useful mental model is a single bottleneck queue. If the sender's rate is below the bottleneck's service rate, ACKs come back steadily and cwnd can grow. When aggregate senders exceed the bottleneck, the queue absorbs the burst for a while; eventually either delay grows or packets are dropped. Classic TCP treats packet loss as the congestion signal. On loss, it performs multiplicative decrease: reduce the sending window by a factor, classically one half, and set ssthresh to that reduced value. The full AIMD rule is therefore:

  • Additive increase: grow by about one SMSS per RTT during congestion avoidance.
  • Multiplicative decrease: on congestion, cut the window sharply, traditionally to about half.
  • Slow start: after connection start, timeout recovery, or a low threshold, grow exponentially until ssthresh is reached.
The Reno AIMD sawtooth, with ssthresh tracking half the prior peak.
The Reno AIMD sawtooth, with ssthresh tracking half the prior peak.

The stability argument is simple but powerful. Additive increase lets competing flows approach capacity without synchronized explosions. Multiplicative decrease drains queues quickly after overshoot. If two Reno-like flows share a bottleneck and see the same losses, the larger flow gives up more absolute bandwidth when both halve their windows, which tends to pull the system toward fairness over repeated cycles. It is not perfect: RTT differences, ACK behavior, offloads, queue policy, and application pacing all matter. But AIMD was the first robust rule that made the public Internet survivable under distributed, uncoordinated senders.

AIMD fairness: two flows on a shared bottleneck converge toward an equal share.
AIMD fairness: two flows on a shared bottleneck converge toward an equal share.

Tahoe, Reno, and NewReno refine what happens after loss. TCP Tahoe introduced the core ideas: slow start, congestion avoidance, and fast retransmit. On detecting loss, Tahoe sets ssthresh to about half the current flight, collapses cwnd to one segment, retransmits, and re-enters slow start. This is reasonable after a retransmission timeout, where the ACK clock may have stopped. But after duplicate ACKs, the receiver is still generating feedback, which implies later packets are reaching the destination.

Reno exploits that distinction with fast recovery. After the usual duplicate-ACK trigger for fast retransmit, Reno retransmits the missing segment, sets ssthresh to about half the flight, and avoids collapsing all the way to one segment. The duplicate ACKs are treated as evidence that packets have left the network and reached the receiver, so the sender can keep data moving. This is the historical shorthand: Tahoe falls back to slow start on this kind of loss; Reno halves and recovers.

On loss: Tahoe drops cwnd to 1; Reno halves it (fast recovery).
On loss: Tahoe drops cwnd to 1; Reno halves it (fast recovery).

NewReno fixes an important Reno weakness when multiple packets are lost from the same window. A Reno sender can leave fast recovery too early when it receives a partial ACK: an ACK that advances the cumulative acknowledgment point but does not cover all data outstanding when recovery began. NewReno treats a partial ACK during fast recovery as evidence that the next unacknowledged segment is probably lost, retransmits it, and remains in fast recovery until all data from the recovery window has been acknowledged. This is still cumulative-ACK TCP, not SACK-based precision, but it avoids unnecessary timeouts under multiple losses.

NewReno: a partial ACK during fast recovery triggers retransmit and keeps recovery open.
NewReno: a partial ACK during fast recovery triggers retransmit and keeps recovery open.

For low-level systems work, cwnd gates real transmit descriptors, DMA mappings, TSO super-packets, pacing decisions, and wakeups. A driver may see a few large TSO buffers while the TCP stack accounts in bytes and segments. A NIC may coalesce completions while the congestion controller is paced by ACKs arriving on another CPU. None of that changes AIMD's contract, but debugging throughput requires looking at bytes in flight, not merely packets handed to the NIC. A 100 Gb/s link with a 100 microsecond RTT has a bandwidth-delay product around 1.25 MB; a small cwnd or a reset to slow start can underfill the transmit queue.

cwnd vs BDP: a too-small window underfills the transmit queue on a 100 Gb/s, 100 us path.
cwnd vs BDP: a too-small window underfills the transmit queue on a 100 Gb/s, 100 us path.

The exponential-then-linear shape is therefore not a historical curiosity. Slow start rapidly discovers an order-of-magnitude capacity estimate. Congestion avoidance continues probing once the sender is close enough that mistakes are expensive. Multiplicative decrease backs off fast enough for shared queues to recover. Tahoe, Reno, and NewReno differ in recovery details, but they share the same control-loop idea: use ACKs to clock growth, use loss to mark overshoot, and converge toward a rate the path can carry.

Sources

8.3 Loss as a signal: fast retransmit and recovery

Loss-based TCP control treats missing bytes as evidence that the path has been overfilled. If a router queue overflows, a packet is dropped; if a packet is dropped, the sender should reduce its sending rate. Fast retransmit and fast recovery let TCP react without waiting for a retransmission timeout. Loss recovery is therefore not just reliability work. It is where the congestion controller decides whether to keep the ACK clock alive at a reduced rate or fall back to slow start.

Duplicate ACKs as a hint: TCP acknowledgments are cumulative. If the receiver has bytes through sequence X, and then receives data above X while byte X+1 is still missing, it cannot advance the ACK number. Instead it sends another ACK for X, often with SACK information if negotiated. From the sender's point of view, repeated ACKs for the same left edge mean: "the receiver is still missing the next byte, but later data has arrived." That is a strong hint that one segment was lost while later segments continued through the network.

A lost segment freezes the cumulative ACK, so later arrivals re-ACK the same left edge.
A lost segment freezes the cumulative ACK, so later arrivals re-ACK the same left edge.

It is only a hint. Later data could have arrived first because the network reordered packets, a NIC path split traffic across queues, a tunnel changed paths, or duplication occurred. Still, the classic rule is pragmatic: after 3 duplicate ACKs arrive without an intervening ACK that advances SND.UNA, the sender assumes the segment at SND.UNA is missing and retransmits it immediately. This is fast retransmit: repair the likely hole before the retransmission timer expires.

Three duplicate ACKs trigger retransmit before the RTO timer fires.
Three duplicate ACKs trigger retransmit before the RTO timer fires.

The reason for waiting for three duplicate ACKs instead of one is to avoid punishing harmless reordering. One duplicate ACK is common; two may still be a path artifact; three imply that later segments reached the receiver while the earlier one did not. In a trace, this appears as a run of ACKs with the same ack_seq, often with changing SACK blocks describing bytes received above the gap.

One or two duplicate ACKs may be reordering; the third crosses the threshold to act.
One or two duplicate ACKs may be reordering; the third crosses the threshold to act.

Fast recovery: Fast retransmit repairs the missing segment; fast recovery decides how much the sender may transmit while waiting for the repair ACK. Reno's key observation is that duplicate ACKs prove that some data has left the network. The receiver can only generate a duplicate ACK after receiving a segment above the gap, so each duplicate ACK says one segment is no longer occupying queue or link capacity. The sender should reduce its window because loss suggests congestion, but it need not drain the pipe to zero.

In Reno-style recovery, when the third duplicate ACK arrives, TCP sets ssthresh to roughly half the outstanding data, bounded at max(FlightSize / 2, 2*SMSS). It retransmits the missing segment and sets cwnd = ssthresh + 3*SMSS. The extra 3*SMSS is artificial inflation for the three later segments presumed to have left the network. For each additional duplicate ACK, TCP increments cwnd by SMSS; if cwnd and the receiver window permit, the sender may transmit another segment. This preserves ACK-clocked transmission at a reduced rate.

Fast recovery inflates cwnd per dup-ACK, then deflates on the recovery ACK.
Fast recovery inflates cwnd per dup-ACK, then deflates on the recovery ACK.

When an ACK finally acknowledges new data, Reno deflates the window by setting cwnd back to ssthresh and resumes congestion avoidance. If the retransmitted segment was the only loss, the cumulative ACK jumps over the repaired gap and covers later data already buffered at the receiver. With multiple losses, basic Reno struggles. The first repair may produce a partial ACK that advances only to the next missing segment. NewReno improves this by staying in recovery on partial ACKs, but without SACK it still learns mostly from the cumulative ACK edge.

NewReno: a partial ACK repairs the next hole and stays in recovery instead of deflating early.
NewReno: a partial ACK repairs the next hole and stays in recovery instead of deflating early.

SACK recovery: Selective Acknowledgment changes the sender's visibility. With SACK permitted, the receiver can say, in effect: "I still cumulatively ACK through X, but I also have these byte ranges above X." The sender maintains a scoreboard of sequence ranges that are cumulatively ACKed, selectively ACKed, retransmitted, believed lost, or still possibly in flight. RFC 6675 describes this as an explicit per-connection data structure used to update state from each ACK, estimate pipe, and choose the next segment to send.

That scoreboard makes multiple-loss recovery practical. Suppose a 64-segment flight loses segments 10, 20, and 21, while most later segments arrive. Without SACK, the sender sees duplicate ACKs for segment 10, repairs it, and discovers the next gap only after the cumulative ACK moves. With SACK, the sender can see that bytes beyond 20 and 21 are already present. It can mark the holes lost and retransmit them during the same recovery episode, constrained by the reduced congestion window and pipe estimate. SACK says what likely needs repair; cwnd, ssthresh, and recovery rules limit how aggressively TCP may send.

A SACK scoreboard repairs several holes in one window without timeouts.
A SACK scoreboard repairs several holes in one window without timeouts.

This matters in host networking code because the sender's transmit queue is not a simple FIFO. A TCP stack must keep descriptors for unacknowledged data, tag retransmitted ranges, avoid freeing SACKed bytes until cumulative ACK advances, and decide whether a TSO/GSO aggregate maps cleanly to retransmission units. Driver and NIC offloads can hide packet boundaries unless the stack keeps enough metadata. On receive, GRO/LRO, delayed ACK behavior, and out-of-order queues affect duplicate ACK and SACK timing.

The transmit queue tags each segment ACKed, SACKed, retransmitted, or lost β€” not a plain FIFO.
The transmit queue tags each segment ACKed, SACKed, retransmitted, or lost β€” not a plain FIFO.

Timeout is the slow path: A retransmission timeout is different from fast retransmit. Fast retransmit fires while ACK feedback is still arriving; the ACK clock is alive, so fast recovery can keep data moving at a reduced rate. An RTO means the sender has not received timely feedback for the oldest outstanding data. TCP therefore takes a much more conservative action: retransmit after the timer, set ssthresh from half the outstanding flight, collapse cwnd to one full-sized segment, and use slow start to rebuild. This is why tail losses and small application-limited transfers are latency-sensitive: there may not be enough later packets to produce three duplicate ACKs.

Fast retransmit keeps cwnd high; an RTO timeout collapses it to 1.
Fast retransmit keeps cwnd high; an RTO timeout collapses it to 1.

The deeper weakness is that loss is ambiguous. A dropped packet in a full router queue really is congestion evidence. A corrupted wireless frame, reordering on a multipath fabric, a delayed interrupt, or a transient driver queue stall can produce the same duplicate-ACK pattern without bottleneck overflow. Classic loss-based TCP cannot perfectly distinguish these cases. It backs off because doing so is stable for the shared Internet, but the price is needless throughput loss where non-congestion loss or reordering is common. SACK reduces the repair cost. Timestamps and modern time-based loss detection such as RACK use transmission time and SACK feedback to avoid some duplicate-ACK counting failures. Later delay-based and model-based controllers go further by asking whether queues are building before loss occurs.

Loss is ambiguous: corruption or reordering can be misread as congestion.
Loss is ambiguous: corruption or reordering can be misread as congestion.

Sources

8.4 CUBIC and the high-BDP era

Reno's congestion avoidance rule was designed for an Internet where adding roughly one segment per round trip was a reasonable way to search for capacity. After a loss, Reno halves cwnd; while there is no further loss, it grows cwnd linearly by about one SMSS per RTT. The problem is not correctness. The problem is scale.

Reno AIMD: linear +1 SMSS per RTT, then halve cwnd on every loss.
Reno AIMD: linear +1 SMSS per RTT, then halve cwnd on every loss.

The sender's useful target is the bandwidth-delay product: BDP = bottleneck_bandwidth * RTT. A TCP sender needs roughly that much data in flight to keep the path full. On a 10 Gbit/s path with a 100 ms RTT, the BDP is about 125 MB, or roughly 85,000 full-sized 1460-byte segments. If Reno loses at that point and cuts its window in half, it must climb back by about 42,500 segments. At one segment per RTT, that takes about 4,250 s, or more than an hour.

The target is the bandwidth-delay product; the window must reach it to fill the pipe.
The target is the bandwidth-delay product; the window must reach it to fill the pipe.

That arithmetic is why high-BDP paths changed the congestion-control problem. A fat, long pipe is filled by maintaining a very large flight size while still backing off when the path says it is overloaded. If the control loop is too timid after every loss, the link spends most of its time below capacity, even when the path has plenty of bandwidth and low corruption.

CUBIC keeps the same broad loss-based contract as Reno: loss, or an ECN congestion indication when enabled, means reduce the sending rate. The important change is how the sender grows cwnd after that reduction. Instead of making the window a linear function of ACK rounds, CUBIC makes the target window a cubic function of real elapsed time since the last congestion event:

W_cubic(t) = C * (t - K)^3 + W_max

Here W_max is the congestion window just before the last reduction, t is time since the congestion-avoidance epoch began, C is the CUBIC scaling constant, and K is chosen so the curve reaches W_max on time. The current Standards Track CUBIC RFC specifies C = 0.4 and recommends beta_cubic = 0.7 for multiplicative decrease.

K positions the inflection so the cubic curve crosses Wmax exactly when t = K.
K positions the inflection so the cubic curve crosses Wmax exactly when t = K.

The shape matters more than the formula. After a loss, CUBIC remembers the old high-water mark, W_max, and reduces cwnd less sharply than Reno. Reno's classic multiplicative decrease factor is 0.5; CUBIC's beta of about 0.7 leaves the sender at roughly 70% of the pre-loss flight size. If the old window was near the real BDP, cutting to 50% creates a large empty space in the pipe. Cutting to 70% still relieves pressure, but it does not throw away as much discovered capacity.

CUBIC remembers Wmax: cut to ~0.7 Wmax on loss, then climb back to it.
CUBIC remembers Wmax: cut to ~0.7 Wmax on loss, then climb back to it.

From that reduced point, the cubic curve has three intuitive phases.

  • Below W_max, growth is concave. CUBIC increases quickly at first because it is probably reclaiming capacity it already proved the path could carry.
  • Near W_max, the curve flattens into a plateau. This is the cautious region: the sender is close to the last observed saturation point, so it avoids charging through it with large bursts.
  • Above W_max, growth becomes convex. Now CUBIC is probing for new capacity. If competing flows left, routing changed, or the bottleneck rate increased, the sender can discover a higher operating point.
CUBIC's window: concave climb to Wmax, plateau, then convex probing.
CUBIC's window: concave climb to Wmax, plateau, then convex probing.

This is a better fit for high-BDP links than Reno's sawtooth. Reno treats every post-loss recovery as a long linear walk. CUBIC treats the last maximum as useful information: return toward that point efficiently, hover around it, then probe beyond it carefully. The plateau is not wasted time. It is a stabilizer around the most likely bottleneck capacity, reducing the chance that many flows overshoot together and synchronize losses.

On a high-BDP link, CUBIC fills the pipe fast where Reno crawls.
On a high-BDP link, CUBIC fills the pipe fast where Reno crawls.

CUBIC is still ACK-clocked in the implementation sense: the sender normally updates cwnd when ACKs arrive, and it still relies on the transport's loss recovery machinery. But the growth target is keyed to wall-clock time rather than to one additive step per RTT. That distinction is central to RTT-independence. In Reno, a flow with a shorter RTT gets more increase opportunities per second. Two flows sharing a bottleneck but seeing different RTTs do not grow at the same rate in real time; the short-RTT flow tends to be more aggressive because its control loop runs more often. CUBIC's cubic target is based on elapsed seconds since the last loss, so outside its Reno-friendly region, flows with different RTTs have much more similar window growth in time.

Reno favours short-RTT flows; CUBIC's real-time growth is fairer.
Reno favours short-RTT flows; CUBIC's real-time growth is fairer.

This does not mean every flow gets identical throughput regardless of RTT. Throughput is still approximately cwnd / RTT, and real paths include asynchronous losses, delayed ACKs, offloads, pacing, and queue effects. The narrower claim is that CUBIC avoids making the window-increase law itself proportional to RTT frequency. The RFC describes this as an RTT-fairness improvement over Reno, especially where synchronized loss would otherwise strongly favor shorter RTTs.

There is a low-level implementation lesson here: congestion control determines how much data the kernel, NIC, and driver are asked to keep moving. A CUBIC flow on a large-BDP path may legitimately need tens or hundreds of megabytes in flight. That interacts with TCP segmentation offload, generic segmentation offload, large receive offload, interrupt moderation, pacing timers, socket buffer sizing, DMA mapping pressure, and ring occupancy. If a send path batches too aggressively and releases a burst after a scheduling delay, CUBIC may see the resulting queue overflow as congestion even though the bottleneck could have carried a paced stream. Modern Linux therefore combines congestion-control choices with pacing, queue disciplines such as fq, accurate byte accounting, and careful timestamping.

A bursty release overflows the bottleneck queue; pacing spreads it so it fits.
A bursty release overflows the bottleneck queue; pacing spreads it so it fits.

CUBIC became the Linux default because it matched the Internet that servers were increasingly operating on: higher access rates, longer paths, larger socket buffers, and enough deployment experience to show that Reno's conservatism was leaving bandwidth unused. Its predecessor, BIC-TCP, had already been selected as a Linux default for high-speed networks, but BIC was more abrupt and less friendly. CUBIC kept the idea of rapidly finding a remembered operating point while replacing binary search behavior with a smoother cubic curve.

The practical mental model is this: the BDP is the window needed to fill the pipe; loss marks a recent upper bound; beta decides how far below that bound to retreat; and the cubic curve decides how to return, hover, and probe. Reno's +1/RTT growth is a good teaching model for AIMD, but it is too slow to be the main workhorse for fast long-distance networks. CUBIC is the engineering compromise that made loss-based TCP scale into the high-BDP era without abandoning the basic feedback loop that made TCP congestion control deployable in the first place.

Sources

8.5 BBR and delay-based control

Loss is a blunt signal. A dropped packet proves that some queue ran out of room, but it does not say when congestion began. By the time loss-based TCP reacts, the bottleneck queue may already be full. Reno and CUBIC can achieve high utilization while also creating large standing queues because they increase inflight data until the network says "enough" by dropping packets or marking ECN.

Loss-based control only reacts once the buffer overflows β€” by then the queue is already full.
Loss-based control only reacts once the buffer overflows β€” by then the queue is already full.

Delay-based control asks a more sensitive question: is the path getting slower before it starts dropping? TCP Vegas is the classic example. Vegas keeps the best observed RTT as a proxy for propagation delay with little queueing. It compares expected throughput, roughly cwnd / baseRTT, with actual throughput, roughly cwnd / currentRTT. If the current RTT rises while cwnd is unchanged, the extra time is usually queueing delay. Vegas interprets the gap as queued data: if the gap is small, increase; if large, back off before loss.

Delay-based control (Vegas): rising RTT signals a queue β€” back off before loss.
Delay-based control (Vegas): rising RTT signals a queue β€” back off before loss.

That idea treats a queue as information, not just storage. The problem is coexistence. A Vegas flow that reduces its window when delay rises may share a bottleneck with Reno or CUBIC, which treats the same rising delay as harmless until loss. The loss-based flow keeps pushing; the delay-based flow retreats. In a mixed Internet, delay sensitivity can become a disadvantage.

Coexistence trap: the delay-based flow yields bandwidth while the loss-based flow keeps pushing.
Coexistence trap: the delay-based flow yields bandwidth while the loss-based flow keeps pushing.

BBR, short for Bottleneck Bandwidth and Round-trip propagation time, takes a different route. It is not purely delay-based like Vegas, and it is not loss-based like Reno/CUBIC. It is model-based. BBR continuously estimates two physical properties:

  • BtlBw: the bottleneck delivery rate, inferred from recent ACK-clocked delivery-rate samples.
  • RTprop: the minimum round-trip propagation time, inferred from the lowest RTT sample seen over a recent time window.

If those estimates are right, the data needed to fill the path but not a persistent queue is their product: BDP = BtlBw * RTprop. Below that inflight level, the pipe is not full. At about one BDP, the bottleneck can run at full rate with little queue. Beyond one BDP, extra packets mostly sit in the bottleneck buffer, increasing RTT without increasing delivery rate. This is the "knee" of the delivery-rate versus inflight curve: throughput is near its maximum, while queueing delay is near its minimum. BBR tries to operate near that Kleinrock optimal operating point.

The Kleinrock optimum: max delivery rate at the BDP knee, before the buffer fills.
The Kleinrock optimum: max delivery rate at the BDP knee, before the buffer fills.

This framing changes the control problem. A loss-based sender asks, "how much can I put in flight before something drops?" BBR asks, "what rate is the bottleneck delivering, and how much inflight data sustains that rate?" It uses pacing as a first-class control: a pacing rate derived from BtlBw and an inflight cap derived from the estimated BDP. On Linux, this relies on kernel pacing machinery; timestamp quality, ACK processing, segmentation offload, queue disciplines, driver transmit rings, and NIC rate behavior can all affect the packet timing seen on the wire.

BBR paces at BtlBw x RTprop β€” its running estimate of the path.
BBR paces at BtlBw x RTprop β€” its running estimate of the path.

BBR's state machine exists because BtlBw and RTprop cannot be measured perfectly at the same time. To find bandwidth, a sender must sometimes send faster than its current estimate, creating a queue that hides propagation delay. To measure RTprop, it must sometimes reduce inflight data enough for queues to drain.

Why a state machine: probing bandwidth hides RTprop; measuring RTprop hides BtlBw β€” they trade off.
Why a state machine: probing bandwidth hides RTprop; measuring RTprop hides BtlBw β€” they trade off.

STARTUP is the initial bandwidth search. BBR raises its sending rate rapidly, looking for growth in delivered bandwidth. Once delivery-rate growth stalls, BBR concludes it has likely found the bottleneck rate, but it may have overshot and built a queue.

DRAIN follows. BBR deliberately paces below the estimated bottleneck rate to drain the queue created during STARTUP. The goal is to move from "we found the pipe" to "we are no longer standing on queued packets."

PROBE_BW is the normal long-lived mode. BBR cycles its pacing gain around the estimated bottleneck rate: sometimes above to test for more bandwidth, sometimes below to drain probe-created queue, and often near one times BtlBw to cruise. This is a control loop, not a fixed limiter. Competing traffic arrives and leaves, wireless links shift rates, and virtualized paths can change service rate. BBR keeps sampling ACK-clocked delivery rate and updating the model.

PROBE_RTT refreshes the propagation-delay estimate. Since RTprop is the minimum RTT observed when queues are low, a continuously busy flow may stop seeing clean samples. BBR occasionally reduces inflight data so standing queues can drain and a fresh low RTT can be observed. This costs a short throughput dip, but avoids stale path information.

BBR's cycle: STARTUP, DRAIN, PROBE_BW, PROBE_RTT.
BBR's cycle: STARTUP, DRAIN, PROBE_BW, PROBE_RTT.

The difference from loss-based control is clearest on a high-BDP path with deep buffers. A CUBIC flow can increase until a large buffer finally overflows, at which point the queue may represent many milliseconds or seconds of excess delay. That hurts RPC tail latency, interactive traffic, and incast recovery long before bulk throughput looks bad. BBR attempts to stop near the BDP instead of the buffer limit, so it can do well where random loss is not congestion, buffers are shallow, or bufferbloat would dominate latency.

Loss-based control bloats the buffer; BBR holds near the BDP with low latency.
Loss-based control bloats the buffer; BBR holds near the BDP with low latency.

But BBR is not magic fairness dust. Its behavior depends on measurement, pacing, and coexistence. Early BBR versions could be unfair to CUBIC or to other BBR flows in some RTT and buffer regimes. ACK aggregation, policers, token-bucket shapers, and delayed ACKs can distort delivery-rate samples. If pacing is implemented poorly, bursts can reappear below TCP: large TSO/GSO segments, deep driver queues, interrupt moderation, or NIC offloads can turn a smooth logical rate into clumps on the wire.

A logically smooth paced rate becomes bursty clumps below TCP β€” TSO/GSO, driver queues, and coalescing reclump it.
A logically smooth paced rate becomes bursty clumps below TCP β€” TSO/GSO, driver queues, and coalescing reclump it.

These details matter in datacenters. BBR's model is attractive because it tries to minimize standing queues while preserving throughput, but fabrics often use additional signals such as ECN and algorithms such as DCTCP because microsecond-scale queues, shallow switch buffers, and many-to-one incast require fast feedback. A host TCP stack, NIC driver, and switch AQM policy are one control system. If the host holds packets in a qdisc or driver ring after TCP thinks they were paced, TCP's model includes local buffering. If ACK timestamps are noisy because of batching, GRO/LRO, or interrupt coalescing, the delivery-rate estimator can be biased.

Host TCP, qdisc, driver ring, NIC, and switch AQM form one feedback loop β€” local buffering distorts the model.
Host TCP, qdisc, driver ring, NIC, and switch AQM form one feedback loop β€” local buffering distorts the model.

The deeper lesson is that congestion control is not only an algorithm in tcp_cong.c. It is a distributed feedback loop implemented with clocks, queues, DMA rings, ACK generation, offloads, switch buffers, and send patterns. Vegas showed that rising RTT contains early congestion information. BBR showed that a sender can model the pipe: estimate the bottleneck rate, estimate propagation delay, pace near BtlBw, and keep inflight data near BtlBw * RTprop. The debates are about measurement accuracy, probing aggressiveness, and fairness when control laws share one bottleneck.

Sources

8.6 Fairness, AQM, and datacenter congestion

Fairness under additive increase. AIMD has a useful geometric property when competing flows run the same algorithm over the same bottleneck and see roughly the same RTT. Imagine two long-lived Reno-like flows sharing one link. Each RTT, both increase cwnd by about one MSS. The offered load walks upward until the queue overflows or the AQM marks congestion. Then both flows multiplicatively reduce. If one flow has a larger window, the same cut removes more bytes from it than from the smaller flow. Repeating this cycle pulls the pair toward equal rates while keeping the bottleneck busy.

AIMD drives competing flows toward an equal share of the bottleneck.
AIMD drives competing flows toward an equal share of the bottleneck.

That fairness is conditional. A flow with a shorter RTT runs the additive-increase loop more often per second, so classic TCP is biased toward short-RTT paths. Fairness also breaks when algorithms interpret the same signal differently. Reno halves on loss; CUBIC usually reduces to 0.7 * W_max; BBR may not treat isolated loss as proof that the bottleneck rate fell; DCTCP reduces in proportion to ECN marking fraction. Mixing them in one queue can be unfair. For low-level engineers, a packet capture that says "TCP" is not enough: congestion module, RTT, ECN negotiation, pacing, offload behavior, and queue discipline all shape the actual rate.

Same queue, different reactions: a short-RTT flow and a hard-cutting flow grab unequal shares.
Same queue, different reactions: a short-RTT flow and a hard-cutting flow grab unequal shares.

Queue management. The simplest router or switch policy is drop-tail: enqueue until the buffer is full, then drop arriving packets. This absorbs bursts, but it gives endpoints feedback only after the queue has become a latency machine. Large buffers can hide congestion for many RTTs, producing bufferbloat: high standing delay, jitter, and synchronized losses.

A big FIFO bloats latency; AQM (CoDel) drops early to keep the queue short.
A big FIFO bloats latency; AQM (CoDel) drops early to keep the queue short.

Active Queue Management moves the signal earlier. RED, the classic Random Early Detection algorithm, keeps an exponentially weighted average queue length. Below a minimum threshold it does nothing. Above a maximum threshold it drops or marks every arriving packet. Between the thresholds it drops or marks with increasing probability. The idea is to avoid hard overflow and synchronized loss, while making a flow's chance of receiving a signal roughly proportional to its queue share. RED separated "the buffer is out of memory" from "tell senders to slow down," but its thresholds were hard to tune.

RED: drop probability ramps from zero to one between the min and max average-queue thresholds.
RED: drop probability ramps from zero to one between the min and max average-queue thresholds.

Modern AQM controls delay more directly. CoDel measures how long packets sit in the queue and reacts when the minimum sojourn time over an interval remains above a target; the minimum filters out harmless short bursts. PIE also targets average queueing delay, adjusting drop or mark probability with a lightweight control loop. FQ-CoDel adds flow queueing: packets are classified into per-flow queues, served with a deficit round-robin style scheduler, and CoDel runs inside those queues. This gives sparse flows, RPCs, ACKs, and interactive traffic isolation from bulk transfer head-of-line blocking.

FQ-CoDel: packets are hashed into per-flow queues and served round-robin, so a sparse flow skips the bulk backlog.
FQ-CoDel: packets are hashed into per-flow queues and served round-robin, so a sparse flow skips the bulk backlog.

On Linux hosts, this reaches into the driver boundary. Byte Queue Limits, transmit ring sizing, sch_fq, fq_codel, pacing, TSO/GSO segmentation, and NIC completion timing all affect where packets wait. If too much data sits in a device ring, the kernel qdisc may have no opportunity to pace, drop, or mark intelligently.

Byte Queue Limits cap bytes in the NIC ring so packets wait in the qdisc, where AQM can still act.
Byte Queue Limits cap bytes in the NIC ring so packets wait in the qdisc, where AQM can still act.

ECN as lossless signalling. Explicit Congestion Notification gives AQM a cleaner signal path. With ECN negotiated, the sender marks IP packets as ECN-capable using ECT(0) or ECT(1). A congested router or switch can set the CE codepoint instead of dropping the packet. The TCP receiver echoes the signal with ECE; the sender reduces its congestion window and acknowledges that response with CWR. The payload was delivered, so congestion was signalled without retransmission.

ECN: the router marks CE instead of dropping; the sender reduces on the echo.
ECN: the router marks CE instead of dropping; the sender reduces on the echo.

ECN does not eliminate congestion control. It changes the evidence. A CE mark means "this packet crossed a congested queue"; the sender must still reduce load. It also depends on host stack, tunnels, NIC offload path, switch AQM profile, and middleboxes preserving the bits. In datacenters, marking thresholds and endpoint behavior can be engineered together.

Datacenter congestion. Datacenter networks have small RTTs, high link rates, shallow switch buffers relative to bandwidth, and bursty workloads: RPC fan-in, storage reads, distributed training collectives, and cache misses. A few dozen senders can synchronize on one receiver and overflow the top-of-rack switch queue before classic TCP infers anything from loss. This is incast. The issue is not average utilization; it is many flows becoming runnable at once and converging on one egress port or host.

Incast: many synchronized senders overwhelm one switch buffer; DCQCN/PFC respond.
Incast: many synchronized senders overwhelm one switch buffer; DCQCN/PFC respond.

DCTCP was designed for that environment. Switches mark packets with CE once a shallow queue threshold is crossed. The receiver reports ECN marks, and the sender estimates the fraction of marked packets, often represented as alpha. Instead of halving cwnd on one congestion indication, DCTCP reduces proportionally: heavy marking causes a large reduction, light marking causes a small one. The control loop can keep queues short while maintaining high throughput, but it assumes consistent ECN support and carefully chosen thresholds. RFC 8257 warns that DCTCP and conventional TCP should usually be segregated because they do not share capacity like identical AIMD flows.

DCTCP reacts in proportion to the fraction of ECN-marked packets.
DCTCP reacts in proportion to the fraction of ECN-marked packets.

RDMA over Converged Ethernet raises the stakes. RoCE is attractive for HPC and AI fabrics because the NIC can move data directly between application buffers with low CPU overhead, but RoCEv2 runs over UDP/IP and expects a very low-loss Ethernet fabric. Packet loss can be far more expensive than with ordinary TCP because recovery may involve NIC transport state or upper-layer stalls. Operators therefore deploy Priority Flow Control, PFC, from IEEE 802.1Qbb. PFC pauses a traffic class on a link before buffers overflow.

RoCEv2: the NIC writes directly into remote app buffers, and PFC pauses a priority class before the fabric can drop.
RoCEv2: the NIC writes directly into remote app buffers, and PFC pauses a priority class before the fabric can drop.

PFC is blunt. It is link-local, not end-to-end; it pauses a priority class, not just the guilty flow; and if misconfigured it can spread congestion backward or create head-of-line blocking. DCQCN, Data Center Quantized Congestion Notification, combines ECN marking in switches with end-host/NIC rate control for RoCEv2. The switch marks congestion, the receiver returns congestion notification, and the sender reduces and later increases its injection rate. PFC remains as a safety net, while ECN/DCQCN should do the normal control work early enough that pause frames are rare.

PFC pause frames propagate hop-by-hop and block innocent flows; DCQCN's ECN rate loop acts earlier so pauses stay rare.
PFC pause frames propagate hop-by-hop and block innocent flows; DCQCN's ECN rate loop acts earlier so pauses stay rare.

This is directly in the wheelhouse of Solarflare-style NIC and driver work. The questions are where ECN marks are generated and reflected, how completion queues behave when PFC pauses a class, how hardware rate limiting interacts with software pacing, and what telemetry exposes microbursts before they become drops or stalled GPU collectives. Datacenter congestion control is not just a TCP algorithm in the kernel. It is a feedback system spanning application burst shape, kernel queues, NIC rings, switch buffers, ECN thresholds, pause headroom, and firmware rate control.

Sources