TCP Congestion Control and Fairness
Imagine ten thousand connections sharing a single bottleneck link inside an ISP's network. Each sender knows nothing about the others. It also knows nothing about the link's true capacity. It only knows the round-trip delay from its own measurements. Yet somehow the internet does not collapse into a shouting match. It does not happen that everyone floods the link and nobody gets useful throughput. This is the problem that TCP congestion control solves. It lets every endpoint discover how much data the network can currently absorb. This discovery relies purely on local signals like packet loss and delay. The stakes are large. Without congestion control, a single overloaded router queue triggers a feedback loop of retransmissions. This feedback loop makes congestion worse. This failure mode caused the "congestion collapse" episodes on the early NSFNET in the 1980s. The fix, due largely to Van Jacobson, changed TCP fundamentally. It turned TCP from a protocol that only managed flow control between two hosts. It became one that also manages shared network resources. This happens without any explicit signaling from routers. This is why congestion control belongs conceptually next to TCP Flow Control: flow control protects the receiver, congestion control protects the network. The big picture is a distributed control system. Each TCP sender maintains a congestion window (`cwnd`). This is a self-imposed limit on outstanding unacknowledged data. The sender adapts it using an elegant additive-increase/multiplicative-decrease (AIMD) rule. Acknowledgments serve as the only feedback signal. Loss (or explicit congestion notification) serves as the only sign of trouble. Remarkably, this decentralized, information-poor algorithm converges toward an outcome. It reaches an approximately fair allocation of bandwidth among competing flows sharing a link. This property can be derived mathematically rather than merely observed empirically. This article develops the full mechanism — slow start, congestion avoidance, fast retransmit and fast recovery. It builds the AIMD dynamics from first principles. It proves why AIMD (and not, say, multiplicative-increase/additive-decrease) is the rule that yields fairness and stability. It assumes familiarity with TCP Connection Management and basic window-based flow control.
- TCP infers network congestion indirectly, using packet loss or delay as the only feedback signal, since routers give no explicit "you are congesting me" message in classic TCP.
- The congestion window `cwnd` throttles how much unacknowledged data a sender may have in flight, separate from the receiver's advertised window `rwnd`.
- Slow start grows `cwnd` exponentially until a threshold `ssthresh`, then congestion avoidance grows it linearly (AIMD): +1 MSS per RTT, halved on loss.
- Fast retransmit and fast recovery let TCP react to isolated losses (three duplicate ACKs) without collapsing all the way back to slow start.
- AIMD is not an arbitrary choice: it is the unique linear control law that provably converges to an efficient and fair allocation of bottleneck bandwidth among competing flows.
- Fairness is imperfect in practice: RTT bias, non-AIMD variants (e.g., CUBIC), and unresponsive UDP flows all break the idealized fair-sharing picture.
TCP controls network congestion the same way a driver controls a car in fog: accelerate cautiously, brake hard the instant something goes wrong, and let millions of independent drivers doing exactly this converge to smooth, roughly equal-share traffic flow.
The idea
Congestion control is the mechanism by which a TCP sender limits its transmission rate to avoid overwhelming the network path, as distinct from flow control, which limits the rate to avoid overwhelming the receiver's buffer. The sender tracks two independent limits and always transmits at the more conservative of the two: the receiver-advertised window `rwnd` (flow control) and a self-computed congestion window `cwnd` (congestion control). The actual amount of unacknowledged data allowed in flight is ``` effective_window = min(cwnd, rwnd) ``` Because routers along the path do not tell endpoints their queue occupancy or link capacity (in the classic, non-ECN model), TCP must treat the network as a black box and infer congestion from indirect signals: a lost segment (detected via retransmission timeout or duplicate acknowledgments) is treated as unambiguous evidence that some link on the path is overloaded. This is a deliberate design decision: it requires no cooperation from routers, works across arbitrary heterogeneous networks, and pushed complexity to the edges — consistent with the internet's end-to-end design philosophy. The congestion window evolves through a small state machine with four phases: slow start, congestion avoidance, fast retransmit, and fast recovery. Slow start bootstraps a new (or stalled) connection quickly by growing `cwnd` exponentially, since starting linearly would waste enormous time reaching a large but unknown bottleneck capacity. Congestion avoidance takes over once `cwnd` approaches a remembered estimate of the safe operating point (`ssthresh`), switching to cautious, linear growth. When loss is detected, the sender reacts: a timeout is treated as severe distress and resets to slow start from `cwnd = 1`, while three duplicate ACKs (signaling an isolated loss with the pipe otherwise flowing) trigger the gentler fast retransmit/fast recovery path that only halves the window. This asymmetric response — cautious linear growth, aggressive multiplicative cutback — is precisely the AIMD rule whose convergence and fairness properties can be proven mathematically, not just observed.
How it works
State variables (per TCP connection, sender side):
- `cwnd` — congestion window, in bytes or segments (MSS units); how much unacked data the sender may inject.
- `ssthresh` — slow-start threshold; the boundary between exponential and linear growth.
- `dupACKcount` — count of consecutive duplicate ACKs, used to detect isolated packet loss.
Phase 1 — Slow Start. The connection begins with `cwnd = 1 MSS` (historically; modern TCP uses an initial window of 2–10 MSS per RFC 6928) and `ssthresh` set to some large initial value (often the receiver's advertised window). Every time a new ACK arrives acknowledging previously unacked data, the sender increases `cwnd` by one MSS. Since a full window of segments is acknowledged (roughly) once per RTT, and each of those ACKs bumps `cwnd` by 1 MSS, the net effect is that `cwnd` doubles every RTT — genuine exponential growth. Slow start ends when `cwnd ≥ ssthresh`, at which point the sender switches to congestion avoidance, or immediately if loss occurs.
Phase 2 — Congestion Avoidance. Now growth becomes conservative: `cwnd` increases by roughly `MSS × (MSS / cwnd)` for every ACK received, which sums to an increase of one MSS per RTT — additive increase. This is the linear-growth half of AIMD.
Loss detection and reaction. Two distinct loss signals lead to two distinct reactions: 1. Timeout (RTO fires, no ACK at all). This is treated as a sign of serious congestion (possibly a lost SYN of information, a badly congested or broken path). The sender sets ``` ssthresh = max(cwnd / 2, 2 MSS) cwnd = 1 MSS ``` and re-enters slow start. This is a severe, multiplicative-decrease event. 2. Triple duplicate ACK (3 dupACKs). The receiver, upon getting an out-of-order segment, immediately re-ACKs the last in-order byte it has. Three duplicate ACKs in a row strongly suggest exactly one segment was dropped while later segments got through — the pipe is not empty, just missing one segment. TCP reacts with fast retransmit: resend the missing segment immediately, without waiting for the retransmission timer. Then it enters fast recovery (Reno-style): ``` ssthresh = max(cwnd / 2, 2 MSS) cwnd = ssthresh + 3 MSS # inflate for the 3 dup ACKs already "banked" // for each additional dup ACK: cwnd += MSS (window inflation) // on new ACK (recovery of the lost segment confirmed): cwnd = ssthresh // resume congestion avoidance from here ``` Fast recovery avoids the brutal drop to `cwnd = 1`; the window is halved, not annihilated, because the ACK clock shows the network is still delivering most segments successfully.
Putting it together, full pseudocode: ``` state: cwnd = INITIAL_WINDOW // e.g. 2..10 MSS (RFC 6928) ssthresh = ADVERTISED_WINDOW // large initial value mode = SLOW_START dupACKcount = 0 on receiving a new ACK (acknowledges previously unacked data): dupACKcount = 0 if mode == SLOW_START: cwnd = cwnd + MSS // exponential growth if cwnd >= ssthresh: mode = CONGESTION_AVOIDANCE else if mode == CONGESTION_AVOIDANCE: cwnd = cwnd + MSS (MSS / cwnd) // additive increase, ~1 MSS per RTT else if mode == FAST_RECOVERY: cwnd = ssthresh // deflate: exit recovery mode = CONGESTION_AVOIDANCE on receiving a duplicate ACK: dupACKcount = dupACKcount + 1 if dupACKcount == 3 and mode != FAST_RECOVERY: // fast retransmit retransmit(oldest_unacked_segment) ssthresh = max(cwnd / 2, 2 MSS) // multiplicative decrease cwnd = ssthresh + 3 MSS // window inflation mode = FAST_RECOVERY else if mode == FAST_RECOVERY: cwnd = cwnd + MSS // further inflation per extra dup ACK on retransmission timeout (RTO): ssthresh = max(cwnd / 2, 2 MSS) // multiplicative decrease cwnd = 1 MSS // full reset mode = SLOW_START dupACKcount = 0 retransmit(oldest_unacked_segment) backoff_RTO() // exponential RTO backoff ``` This is essentially TCP Reno. TCP NewReno refines fast recovery to handle multiple losses in one window (partial ACKs keep it in recovery instead of exiting prematurely); TCP CUBIC (Linux default) replaces the linear additive-increase curve with a cubic function of time-since-last-loss for better performance on high-bandwidth paths, while TCP Vegas and delay-based variants use RTT increase, not loss, as the congestion signal.
Complexity & correctness
Time to reach full utilization (slow start). If the bottleneck bandwidth-delay product is `W` segments and slow start doubles `cwnd` each RTT starting from `cwnd₀`, the number of RTTs to reach `W` is ``` n = ceil(log2(W / cwnd0)) ``` This is exponential convergence in the number of round trips, i.e. `O(log W)` RTTs — dramatically faster than the `O(W)` RTTs a naive linear ramp-up would need. This asymptotic gain is exactly why slow start exists: on a high-bandwidth-delay-product path (say `W = 10000` segments), linear ramp-up at 1 MSS/RTT would take 10,000 RTTs to reach capacity, while doubling reaches it in about 14 RTTs.
Steady-state AIMD analysis. In congestion avoidance, `cwnd` grows by 1 MSS per RTT (additive increase, parameter `α = 1`) and is halved on loss (multiplicative decrease, parameter `β = 1/2`). This produces the classic sawtooth: linear ramp, instantaneous halving, repeat. Two facts follow from this shape:
- Average throughput. If the window ranges from `W/2` to `W` before the next loss, sent over `W/2` RTTs (since it takes that many RTT-increments of 1 MSS to go from `W/2` back to `W`), the average window is `0.75 W`, and average throughput is approximately
``` Throughput ≈ (0.75 · W · MSS) / RTT ``` This is the basis of the well-known TCP throughput formula (Mathis et al.), which under a simplified loss model with loss probability `p` gives ``` Throughput ≈ (MSS / RTT) · C / sqrt(p) for a constant C ≈ 1.22 (or sqrt(3/2)) ``` This formula is itself derivable by noting that between two losses the window sweeps a sawtooth of "area" `W²/2` packets sent (a triangular sum), setting that equal to `1/p` (the expected packets between losses), and solving for `W`.
Why AIMD specifically, not some other linear control rule — the fairness argument. Consider two flows sharing a bottleneck, with congestion windows `x₁, x₂` plotted as a point in the plane. The bottleneck link has capacity such that the line `x₁ + x₂ = C` marks full utilization (efficiency), and the line `x₁ = x₂` marks perfect fairness. Chiu and Jain's classical phase-plot analysis considers a general linear control rule: ``` increase: x_i ← x_i + a (additive increase by constant a) decrease: x_i ← b · x_i (multiplicative decrease by factor b < 1) ``` Geometrically, additive increase moves the point `(x₁, x₂)` along a vector parallel to `(1,1)` — this moves the point toward the fairness line `x₁ = x₂` regardless of where it starts, because it adds the same absolute amount to both coordinates, shrinking their relative gap. Multiplicative decrease scales the point toward the origin along the ray from the origin through the current point — this preserves the ratio `x₁/x₂`, neither helping nor hurting fairness, but pulls the sum back under `C` after an overshoot. Formally, define the fairness index (Jain's index) for `n` flows with rates `x_i`: ``` F(x) = (Σ xi)² / (n · Σ xi²) ``` `F = 1` means perfect equality; `F = 1/n` means one flow monopolizes the link. One can show that after an additive-increase step `xi ← xi + a`, the variance of the `xi` (a proxy for unfairness) stays the same in absolute terms while the mean grows, so the coefficient of variation shrinks — the flows converge toward equal share. After a multiplicative-decrease step `xi ← b·xi`, the coefficient of variation is exactly preserved (scaling all values by the same constant does not change relative spread). Hence:
- Additive increase is fairness-improving (strictly, whenever flows are unequal).
- Multiplicative decrease is fairness-neutral.
- Any control law using multiplicative increase (each flow scales up by the same factor) would be fairness-neutral on increase — never correcting inequality — and any law using additive decrease would move the point away from proportional scaling, and (Chiu–Jain show) can actually move flows toward the axes, worsening fairness or even driving one flow's window to zero.
This is why AIMD is essentially the unique choice, among linear control rules, that guarantees convergence to fairness and stays responsive to congestion: every synchronized loss event pulls the aggregate window back under capacity (efficiency), while every increase phase narrows the gap between competing flows (fairness), and iterating this process drives the system toward the intersection of the efficiency line and the fairness line, i.e., equal shares at full utilization.
Correctness invariant. The design maintains a simple invariant across all four phases: the sender never has more unacknowledged data in flight than `min(cwnd, rwnd)` allows, and `cwnd` is monotonically adjusted only in response to legitimate ACK/loss signals, never exceeding what the network has recently proven it can sustain (approximated by the pre-loss window, halved). Because `ssthresh` after a loss is set from the actual window size at the moment of loss, the algorithm is self-calibrating: it does not need to know link capacity in advance, only to remember "the last window size that worked, roughly."
Honest tradeoffs.
- AIMD's sawtooth means throughput never truly stabilizes; it oscillates, which is fine for bulk transfer but adds jitter unsuitable for some real-time uses.
- The `1/sqrt(p)` throughput law implies TCP Reno scales poorly on very high-bandwidth, high-loss-sensitivity paths (the "long fat network" problem), motivating CUBIC and other post-Reno variants.
- Fairness convergence assumes flows share the same RTT; flows with shorter RTT get more frequent additive-increase opportunities per unit time and thus a larger steady-state share — RTT bias is a real, provable unfairness of standard TCP, not a bug in the derivation above but a limitation of the "per-RTT" additive step.
- Loss-based congestion signals conflate genuine congestion with unrelated packet corruption (common on wireless links), causing TCP to throttle unnecessarily on lossy but uncongested paths.
Worked example
Trace a single connection with `MSS = 1000 bytes`, starting `cwnd = 1 MSS`, initial `ssthresh = 16 MSS`, one RTT per row. Assume no losses until RTT 7, where a triple-duplicate-ACK event occurs (not a timeout).
| RTT | Phase entering | cwnd at start (MSS) | Event | cwnd after (MSS) | ssthresh after |
|---|---|---|---|---|---|
| 1 | Slow Start | 1 | full window ACKed | 2 | 16 |
| 2 | Slow Start | 2 | full window ACKed | 4 | 16 |
| 3 | Slow Start | 4 | full window ACKed | 8 | 16 |
| 4 | Slow Start | 8 | full window ACKed | 16 | 16 |
| 5 | cwnd = ssthresh → switch | 16 | Congestion Avoidance begins | 17 | 16 |
| 6 | Congestion Avoidance | 17 | +1 MSS | 18 | 16 |
| 7 | Congestion Avoidance | 18 | 3 dup ACKs detected | fast retransmit; `ssthresh = max(18/2, 2) = 9`; `cwnd = 9 + 3 = 12` | 9 |
| 7 (cont.) | Fast Recovery | 12 | 2 more dup ACKs arrive (inflation) | 14 | 9 |
| 8 | Fast Recovery | 14 | new ACK arrives (loss recovered) | `cwnd = ssthresh = 9`; enter Congestion Avoidance | 9 |
| 9 | Congestion Avoidance | 9 | +1 MSS | 10 | 9 |
| 10 | Congestion Avoidance | 10 | +1 MSS | 11 | 9 |
Reading the trace: RTTs 1–4 show textbook doubling (1→2→4→8→16), confirming the `O(log W)` slow-start convergence claim. At RTT 4→5, `cwnd` hits `ssthresh = 16` and the state machine switches modes exactly as the pseudocode specifies. The dup-ACK event at RTT 7 triggers the Reno fast-recovery formula precisely: `ssthresh` is halved to 9 (from cwnd 18, `18/2=9`), and `cwnd` is inflated to `ssthresh + 3 = 12` to "spend" the three duplicate ACKs already received (each one indicates a segment left the network), then further inflated to 14 by two more duplicate ACKs before the retransmitted segment's ACK arrives and collapses `cwnd` back down to `ssthresh = 9` — the halved value — resuming linear growth from there. This confirms the sawtooth: peak window before loss was 18, and the new steady growth floor is 9, almost exactly half, matching the `β = 1/2` multiplicative-decrease rule.
Second case — timeout instead of dup ACKs. Suppose at RTT 7 instead of dup ACKs, the retransmission timer expires (say, a burst loss dropped multiple segments and no ACK arrived at all). Then per the pseudocode's timeout branch: ``` ssthresh = max(18/2, 2) = 9 cwnd = 1 mode = SLOW_START ``` Now the recovery path is far slower: the sender must climb from `cwnd = 1` all the way back through slow start (doubling per RTT: 1, 2, 4, 8, then switching to congestion avoidance once `cwnd ≥ ssthresh = 9`) before resuming linear growth — roughly 4 more RTTs of ramp-up compared to the single-RTT recovery in the dup-ACK case. This concretely shows why the timeout/dup-ACK distinction matters: it is the difference between an almost-unnoticeable one-RTT dip and a multi-RTT throughput collapse, for what might be the exact same amount of lost data.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Confusing `cwnd` with the advertised window `rwnd` | Students conflate congestion control (network-facing) with flow control (receiver-facing) | Wrong mental model: e.g. assuming a large receive buffer alone gives high throughput on a congested path |
| Treating every loss as a timeout | Ignoring the dup-ACK fast-path | Overestimates how punishing packet loss is; misses why fast recovery exists at all |
| Forgetting `ssthresh` has a floor (`max(cwnd/2, 2·MSS)`) | Naively halving without a floor | On repeated losses, `cwnd` can be driven to zero or below MSS, stalling the connection |
| Assuming AIMD converges to fairness instantly | Treating the Chiu–Jain phase-plot argument as a one-step proof | The convergence is asymptotic over many increase/decrease cycles, and only exact under equal-RTT, synchronized-loss assumptions |
| Ignoring RTT bias | Believing "AIMD ⇒ perfectly equal shares" unconditionally | Short-RTT flows get more additive-increase opportunities per second and provably win a larger long-run share |
| Mixing Reno-style single-loss recovery with multi-segment loss in one window | Basic Reno only detects one loss per RTT via dup ACKs | Can cause repeated unnecessary timeouts; motivated NewReno's partial-ACK handling |
Pioneers & history
Origins · 1 minVan Jacobson and how this was discovered
Van Jacobson diagnosed and fixed the "congestion collapse" episodes that crippled the early NSFNET, where throughput on some links dropped by three orders of magnitude under load. His 1988 paper with Michael Karels, "Congestion Avoidance and Control," introduced slow start, congestion avoidance, and the exponential RTO backoff described in this article — algorithms still running, largely unchanged in spirit, in every modern operating system's TCP stack.
Where this matters
Every TCP-based application on the internet — web browsing (HTTP over TCP or QUIC's TCP-inspired congestion control), file transfer, video streaming's control-plane connections, remote shells, database replication links — depends on some variant of this mechanism to share bandwidth without central coordination. Data center operators tune congestion control variants (DCTCP uses ECN-based signals rather than loss) because Reno-style loss-based control interacts badly with the very short RTTs and shallow buffers typical inside a data center. Content delivery networks and video streaming services care deeply about the sawtooth's amplitude, since large oscillations in `cwnd` translate into bitrate fluctuations users can perceive. Mobile carriers must account for the fact that classic loss-based TCP misinterprets wireless bit-errors as congestion, motivating link-layer retransmission and specialized profiles. The Chiu–Jain fairness framework this article develops is also the standard lens used to evaluate any new congestion control proposal — including Explicit Congestion Notification-based schemes and modern algorithms like BBR — against the baseline AIMD guarantees for both efficiency and fairness.
Ask the tutor: How does explicit congestion notification (ECN) change the fairness and efficiency arguments compared to pure loss-based AIMD?
Why doesn't TCP just ask routers directly how congested they are?
Classic TCP congestion control was designed under the end-to-end principle: keep the network core simple (just forward packets) and push adaptive intelligence to the endpoints. Requiring routers to compute and signal precise congestion levels to every flow would add substantial complexity, state, and trust requirements to core infrastructure that carries traffic from millions of unrelated senders. Instead, TCP treats loss (or delay increase, in some variants) as an implicit signal: if a router's queue overflows, some packet gets dropped, and the resulting missing ACK or duplicate ACK is detectable purely from the sender's own observations. Extensions like Explicit Congestion Notification (ECN) do let routers mark packets before dropping them, giving an explicit but still lightweight signal, but this is an optional enhancement layered on top of, not a replacement for, the core loss-based mechanism.
Is the AIMD sawtooth actually optimal for real bandwidth-delay products today?
Not perfectly. The `1/sqrt(p)` throughput relationship derived from the sawtooth means that as available bandwidth grows, TCP Reno needs an extremely low loss probability to fill the pipe — on a 10 Gbps transcontinental link, Reno would need a packet loss rate far lower than what's realistically achievable, since one loss event forces the sender to spend a huge number of RTTs re-climbing linearly back to full window. This motivated CUBIC, which grows the window as a cubic function of time since the last loss (fast growth right after backing off, plateauing near the previous peak, then accelerating again), giving much better throughput on such "long fat networks" while still preserving reasonable fairness properties. So AIMD's fairness argument remains foundationally correct, but its linear growth rate is now considered too conservative for very high-capacity paths.
Does AIMD guarantee that two flows sharing a bottleneck will get exactly equal bandwidth?
Only under idealized assumptions: equal round-trip times, synchronized loss events (all flows experience loss at the same congestion event), and both flows using the same AIMD parameters. The Chiu–Jain phase-plot argument shows convergence toward the fairness line under these conditions. In practice, RTT differences break this guarantee — a flow with a shorter RTT completes more additive-increase steps per unit time, since increases happen "once per RTT," so it climbs faster and settles at a larger steady-state share. Losses are also rarely perfectly synchronized across flows in a real network with multiple bottlenecks. So TCP's fairness is a strong tendency and a provable asymptotic property under simplified models, not an exact guarantee for arbitrary real traffic mixes.
What's the practical difference between fast recovery and returning to slow start?
Fast recovery halves the congestion window and resumes near that halved value, typically costing at most one or two RTTs of reduced throughput before normal linear growth continues. Returning to slow start resets the window all the way down to 1 MSS and requires climbing back exponentially, which — even though exponential — still costs several RTTs of low throughput before reaching the previous operating point, especially on high-bandwidth-delay-product paths. The difference matters enormously in aggregate: on a path with occasional isolated packet loss, fast recovery keeps throughput close to the achievable maximum, while a naive always-timeout policy would leave substantial bandwidth unused after every single lost packet, even when the network was otherwise healthy.
How does this relate to TCP Flow Control, and can congestion control and flow control conflict?
Flow control (governed by the receiver's advertised window `rwnd`) and congestion control (`cwnd`) are two independent limits, and the sender always respects `min(cwnd, rwnd)`. They do not truly "conflict" because they answer different questions: flow control asks "can the receiver's buffer absorb more data," while congestion control asks "can the network path carry more data." A connection can be flow-control-limited (fast network, slow or busy receiver) or congestion-limited (slow network, receiver with plenty of buffer) at different times, and the effective window formula automatically picks whichever constraint currently binds, so the two mechanisms compose cleanly rather than interfering with each other.
Why is loss such a noisy signal on wireless networks, and what breaks as a result?
TCP's loss-based congestion signal assumes packet loss is caused almost exclusively by router buffer overflow due to congestion. On wireless links (Wi-Fi, cellular), packets are frequently lost due to bit errors, fading, or interference — phenomena entirely unrelated to how much traffic is competing for bandwidth. A standard TCP sender cannot distinguish these causes: it sees a loss and dutifully halves its congestion window (or resets to slow start on a timeout), throttling itself even though the network was not actually congested. This causes substantial, unnecessary throughput loss on lossy wireless paths, motivating link-layer retransmission schemes that hide most bit-error losses from TCP, as well as delay-based or hybrid congestion control algorithms that use RTT increase rather than loss alone as their primary signal.
What is the relationship between ssthresh and cwnd, and why keep both variables?
`cwnd` is the sender's current operating point — how much data it believes it can safely have in flight right now. `ssthresh` is a memory of the last known-good operating point, specifically the window size (halved) at which the connection most recently experienced loss. Keeping them separate lets the sender switch growth strategies precisely: grow exponentially (slow start) while `cwnd` is far below the remembered safe zone, since aggressive probing is unlikely to cause loss there, then switch to cautious linear growth (congestion avoidance) once `cwnd` approaches or exceeds that remembered boundary, where overshoot becomes increasingly likely. Without `ssthresh`, a sender would have no way to know when to stop growing exponentially and start being careful, forcing either perpetually slow linear growth or reckless exponential growth all the way to the next loss.