Core CS · Computer Networks
Nobody tells TCP how fast to send, so it finds out by losing
A router will not warn you that its queue is full. It drops your segment and tells nobody. TCP turns that silence into a control loop: raise the window until something disappears, halve it, and start climbing again. Four rules, two loss signals, and one sawtooth that every interviewer wants you to draw.
Step fifteen round trips of cwnd, Reno against Tahoe →01 The idea
The receiver is not the problem here
Flow control and congestion control both slow the sender down, and that is the only thing they share. Flow control protects the receiver: it stops a fast sender from overrunning a slow receiver’s buffer, and the receiver states its remaining space explicitly in the 16-bit window field of every ACK it sends. Congestion control protects the network in between: it stops all the senders together from overrunning the routers on the path, and no header anywhere carries a number the sender can read off, because no router on the path knows what your share ought to be. There is one narrow exception, a single congestion mark called ECN, and the end of section 03 covers it and why you still cannot rely on it. Keep those two apart and a third of this topic is already done.
So what is congestion, physically? A router has an outgoing link and a queue in front of it. Packets arrive from several inputs and leave on one link. If they arrive faster than the link can carry them away, the queue grows. That is the first symptom and it is not loss at all: it is delay, because every packet now waits behind the ones already queued. The queue is finite memory. Once it is full the router has nothing left to do with the next arrival but drop it. Nothing has failed and nobody is at fault; a perfectly healthy router discarding packets is what an overloaded network looks like.
Now add the thing that makes it dangerous. Every dropped segment is a segment TCP will retransmit, and a retransmission is more traffic offered to the same overloaded router. More offered load means a longer queue, which means more drops, which means more retransmissions. The link stays one hundred per cent busy while the useful throughput, the goodput, falls toward zero, because most of what the link carries is copies of things it already carried. That runaway is congestion collapse, and it is not a thought experiment. In October 1986 the path between Lawrence Berkeley Lab and UC Berkeley, a few hundred metres and three hops, fell from 32 kbit/s to 40 bit/s. Van Jacobson’s answer to that collapse became slow start and congestion avoidance, and they went into TCP in 1988.
Here is the hard constraint that shapes everything after. The network gives the sender no signal. IP is best effort: the router that dropped your segment sends nothing back, the routers upstream of it do not know it happened, and the receiver cannot report a segment it never saw. So TCP watches the only thing it can actually observe, which is whether its own segments are being acknowledged, and it makes one assumption: a lost segment means a full queue. That assumption was true of the wired links TCP was designed for and it is the whole basis of the mechanism. It is also the mechanism’s honest weakness. On a Wi-Fi or mobile link a lost frame usually means interference or a fade, not a full router queue, and TCP halves its window for a reason that does not exist. Say that out loud in an interview; it is the difference between reciting the rules and understanding them.
What TCP raises and lowers is the congestion window, cwnd. It is a variable held only at the sender. It is not advertised, it is not in any header, and the receiver never learns its value. It is the sender’s own running estimate of how much unacknowledged data the path can hold. The sender is then bounded by two limits at once and must respect the smaller: it may have at most min(cwnd, rwnd) bytes in flight, where rwnd is the receiver’s advertised window. One number protects the far machine, the other protects everything between you and it.
02 Worked example
Fifteen round trips, one dropped segment and one timeout
One run, and it is the same run everywhere else in this lesson: the console in section 04 loads it as its first preset, and the round-by-round table in section 03 is this run written out for both TCP versions. The sender opens with cwnd = 1 MSS and ssthresh = 8 MSS. Two things go wrong on purpose, and they are deliberately the two different kinds of wrong: three duplicate ACKs at the end of round 8, and the retransmission timer expiring at the end of round 11. Everything below is TCP Reno. Read left to right.
Written out, Reno gives 1, 2, 4, 8, 9, 10, 11, 12, 6, 7, 8, 1, 2, 4, 5. Add them and the sender has put 90 MSS on the wire across those fifteen round trips. Run the identical loss script under TCP Tahoe instead and you get 1, 2, 4, 8, 9, 10, 11, 12, 1, 2, 4, 1, 2, 3, 4, which totals 74 MSS. The first eight rounds are byte for byte the same, 57 MSS each. The entire 16 MSS gap opens at one moment, the triple duplicate ACK at the end of round 8, and that single divergence is the difference between the two versions.
Slow start is the part of this that gets misread, so fix it now. It is exponential. From 1 MSS it takes ten round trips to reach a window of 1024 MSS, which on a 1460-byte MSS is about 1.5 MB in flight. Nothing about that is slow. The name is historical and it is comparative: before 1988 a TCP sender opened a connection by dumping an entire receiver window onto the path in one burst, and against that, starting from a single segment is slow. Slow start is slow only at the very beginning, and only there.
The highlighted node is the one to sit with, because it is where the two loss signals stop being interchangeable. A duplicate ACK is what a receiver sends when a segment arrives out of order: it re-acknowledges the last in-order byte it has, which is a way of saying “something arrived, but not the piece I am waiting for”. Three of them therefore prove that at least three segments sent after the missing one got all the way across. The path is working. One segment fell out of it. Halving the window is a proportionate response, and waiting for a timer that may be hundreds of milliseconds away would be a waste of a working path.
A timeout is the opposite piece of evidence, and it is evidence by absence. The retransmission timer expired and nothing came back, not a duplicate ACK, not a new ACK, nothing. The sender has lost its ACK clock entirely and no longer has any idea what the path can carry, or whether there is a path. Treating that as severe and dropping to a single segment is not pessimism; it is the sender admitting it has no information left. Everything in section 03 is a formal statement of the difference between those two paragraphs.
03 Mechanics
The four phases, the two loss signals and the two versions
Four tables, in the order the questions arrive. First the phases as a state table, because that is the shape of the question: an interviewer names a state and asks what the rule is and what leaves it. Every rule below is stated per round trip, which is the unit that matters, and the per-ACK form is given alongside because that is what the code actually does.
| Phase | Entered when | What happens to cwnd | Left when |
|---|---|---|---|
| Slow start | The connection opens; after any timeout; after three duplicate ACKs under Tahoe. | cwnd += 1 MSS per ACK, so cwnd doubles per RTT — exponential. |
cwnd reaches ssthresh, or a loss is detected. |
| Congestion avoidance | cwnd reaches ssthresh; or, under Reno, on leaving fast recovery. | cwnd += 1 MSS per RTT, coded as cwnd += MSS × MSS / cwnd per ACK — linear. |
A loss is detected, by either signal. |
| Fast retransmit | Three duplicate ACKs arrive for the same sequence number. | Nothing yet. The missing segment is resent immediately, without waiting for the timer. | At once, into fast recovery under Reno or into slow start under Tahoe. |
| Fast recovery (Reno only) | Straight after fast retransmit. | ssthresh = cwnd / 2, then cwnd = ssthresh + 3 MSS while recovering. |
The ACK for the retransmitted segment arrives; cwnd deflates to ssthresh and congestion avoidance resumes. |
Where that + 3 comes from, and why exams still want cwnd / 2. cwnd is a limit on data in the network. Three duplicate ACKs are three pieces of proof that three segments have left the network and reached the receiver, so the sender is entitled to put three more in without exceeding its new limit. That is the entire justification for the inflation, and each further duplicate ACK adds one more MSS for exactly the same reason. When the ACK for the retransmitted segment finally arrives, the window is deflated back to ssthresh. Written answers almost always want the deflated value, so the safe reply is cwnd = ssthresh = old cwnd / 2, with the inflation named in one clause to show you know why the number moves.
Now the two signals side by side. This is the single most examined table in the topic, because the exam question is nearly always “which of the two happened, and what does each version do about it”.
| Signal | What TCP infers | New ssthresh | Tahoe cwnd | Reno cwnd | Next phase |
|---|---|---|---|---|---|
| Retransmission timeout | severe: nothing at all is getting through |
max(cwnd / 2, 2 MSS) |
1 MSS |
1 MSS |
Slow start, in both versions. |
| Three duplicate ACKs | mild: one segment lost, later ones arriving |
max(cwnd / 2, 2 MSS) |
1 MSS |
ssthresh, i.e. half |
Tahoe: slow start. Reno: congestion avoidance. |
Two details that get dropped. First, ssthresh is never allowed below 2 MSS: RFC 5681 defines it as max(FlightSize / 2, 2 × SMSS), so repeated losses on a tiny window cannot drive it to zero and strand the connection. Second, both versions have fast retransmit. Tahoe introduced it in 1988 and it is not what Reno added. Reno added fast recovery, which is the decision to halve rather than collapse afterwards. Saying “Tahoe has no fast retransmit” is the standard slip and it is worth a mark.
Here is the section 02 run written out for both versions on the identical loss script, which is what the console’s two tabs step through. The two columns are the same for eight rounds and then part company at one row. Every value follows from the two tables above and nothing else.
| Round | Event at the end of this round | Reno — cwnd / ssthresh / phase | Tahoe — cwnd / ssthresh / phase |
|---|---|---|---|
| 1 | — | 1 / 8 / slow start | 1 / 8 / slow start |
| 2 | — | 2 / 8 / slow start | 2 / 8 / slow start |
| 3 | — | 4 / 8 / slow start | 4 / 8 / slow start |
| 4 | cwnd has reached ssthresh | 8 / 8 / cong. avoidance | 8 / 8 / cong. avoidance |
| 5 | — | 9 / 8 / cong. avoidance | 9 / 8 / cong. avoidance |
| 6 | — | 10 / 8 / cong. avoidance | 10 / 8 / cong. avoidance |
| 7 | — | 11 / 8 / cong. avoidance | 11 / 8 / cong. avoidance |
| 8 | three duplicate ACKs | 12 / 8 / cong. avoidance | 12 / 8 / cong. avoidance |
| 9 | the versions diverge here | 6 / 6 / cong. avoidance | 1 / 6 / slow start |
| 10 | — | 7 / 6 / cong. avoidance | 2 / 6 / slow start |
| 11 | retransmission timeout | 8 / 6 / cong. avoidance | 4 / 6 / slow start |
| 12 | — | 1 / 4 / slow start | 1 / 2 / slow start |
| 13 | Tahoe reaches its ssthresh of 2 | 2 / 4 / slow start | 2 / 2 / cong. avoidance |
| 14 | Reno reaches its ssthresh of 4 | 4 / 4 / cong. avoidance | 3 / 2 / cong. avoidance |
| 15 | — | 5 / 4 / cong. avoidance | 4 / 2 / cong. avoidance |
| Total delivered over 15 round trips | 90 MSS | 74 MSS | |
Read row 11 carefully, because it is the one that punishes memorisation. The timeout fires at the same round in both columns, but it does not halve the same number. Reno is sitting at cwnd = 8 and gets ssthresh = 4; Tahoe is only at cwnd = 4, because it spent rounds 9 and 10 climbing back from one, and gets ssthresh = 2. The version that recovered better from the first loss also has more to lose in the second, and it still ends ahead. A loss rule always applies to whatever cwnd happens to be at that instant, never to some remembered earlier value.
Finally the versions, which is a question in its own right and comes up as “which TCP are you describing?”. Everything after Reno keeps the same two signals and changes only the growth curve or the bookkeeping.
| Version | On three duplicate ACKs | On a timeout | What it added |
|---|---|---|---|
| Tahoe 1988 | fast retransmit, then ssthresh = cwnd/2 and cwnd = 1, slow start |
ssthresh = cwnd/2, cwnd = 1, slow start |
Slow start, congestion avoidance and fast retransmit. The original three. |
| Reno 1990 | fast retransmit, then fast recovery: ssthresh = cwnd/2 and cwnd = ssthresh |
identical to Tahoe |
Fast recovery. One rule, and it is the whole examinable difference. |
| NewReno RFC 6582 | as Reno, but stays in fast recovery until every segment outstanding at the loss is acknowledged |
identical to Reno |
Correct handling of more than one loss in the same window, which plain Reno mishandles. |
| CUBIC Linux default | same signal, but growth is a cubic function of time since the last loss |
identical in kind |
Growth that scales on long fat paths, where one MSS per round trip is far too slow to fill the pipe. |
Why AIMD converges on a fair share, and why that is a property of the decrease. Put two flows with the same round trip time through one bottleneck. Additive increase adds the same 1 MSS per round trip to both windows, so it moves them up together and leaves the difference between them exactly as it was. Multiplicative decrease halves both windows, and halving two numbers halves the gap between them too. Repeat that cycle and the gap is driven toward zero while the sum stays pinned near the capacity of the link, so the two flows converge on an equal share without ever exchanging a single message about it. The increase rule keeps the link busy; the decrease rule is where the fairness lives.
Two windows, and the smaller one wins. A sender may have at most min(cwnd, rwnd) bytes unacknowledged. rwnd arrives from the receiver in a 16-bit field, so it tops out at 65,535 bytes unless the two ends negotiated the window scale option from RFC 7323 during the handshake, which multiplies it by a power of two. On a fast modern path the binding limit is almost always cwnd, and a question that gives you both numbers is checking whether you take the minimum or quietly ignore the one you were not thinking about.
And the modern alternative to inferring anything. Explicit Congestion Notification, RFC 3168, lets a router that is about to build a queue mark a packet instead of dropping it, using two bits in the IP header, after which the receiver echoes the mark back to the sender in the ECE flag of its next ACK. The sender then reduces its window exactly as though a segment had been lost, except that nothing was lost and no round trip was spent recovering. It has to be enabled on hosts and on the routers in between, which is why loss is still the signal you will be asked about.
05 Cheat sheet
Twelve answers to have ready on the morning
Every row is something you can be asked to state in under ten seconds. The right-hand column is the specific wrong answer that gets written down, not a general caution.
| What they ask | The answer | The trap |
|---|---|---|
| cwnd when a connection opens | 1 MSS in the textbook | Claiming a real Linux box also starts at 1. It starts at 10, per RFC 6928. |
| Slow start growth rate | +1 MSS per ACK, so cwnd doubles every RTT | calling it slow — it is exponential and it is the fastest phase there is |
| Slow start ends when | cwnd reaches ssthresh, or a loss is detected | waiting for cwnd to exceed ssthresh — reaching it is enough |
| Congestion avoidance growth rate | +1 MSS per RTT | Adding 1 MSS per ACK. That is slow start again, under the wrong name. |
| On a timeout | ssthresh = cwnd / 2, cwnd = 1 MSS, slow start | halving cwnd — a timeout collapses it, it does not halve it |
| On three duplicate ACKs, Reno | ssthresh = cwnd / 2, cwnd = ssthresh, congestion avoidance | resetting cwnd to 1 — that is Tahoe’s answer, not Reno’s |
| On three duplicate ACKs, Tahoe | fast retransmit, then ssthresh = cwnd / 2, cwnd = 1, slow start | Saying Tahoe has no fast retransmit. It has. It has no fast recovery. |
| Floor on ssthresh | never below 2 MSS (RFC 5681) | Halving repeatedly down to 1 or 0 and stranding the connection. |
| How much may be in flight | min(cwnd, rwnd) | quoting cwnd alone — flow control is still running underneath |
| The two halves of AIMD | additive increase +1 MSS/RTT; multiplicative decrease × 1/2 | Putting the additive increase in slow start. It belongs to congestion avoidance. |
| Why loss and not a message | IP is best effort; the router that drops has no way to tell the sender | saying the router informs the sender — only ECN does, and only if enabled |
| Where the loss assumption fails | wireless: a lost frame means interference, not a full queue | Claiming TCP behaves identically over Wi-Fi. It halves for no reason. |
06 Where & why
The same two variables, in four real stacks
None of this is a teaching abstraction. cwnd and ssthresh are fields you can print on a running socket, and the version in use is a string you can change at runtime. Each of the four systems below departs from the textbook in one specific way, and naming that departure is what separates a memorised answer from a used one.
ss -ti prints cwnd: and ssthresh: for every established TCP connection, alongside the measured RTT, so the curve in section 04 is something you can watch on your own machine during a large download. sysctl net.ipv4.tcp_congestion_control shows which algorithm is in use and will say cubic, not reno, on any default install. One real difference to name: Linux opens at an initial window of 10 MSS, following RFC 6928, because on a modern path starting from one segment wastes several round trips on transfers that are over in a few.
BBR was built at Google and merged into Linux in version 4.9. Instead of inferring congestion from loss, it repeatedly measures the bottleneck bandwidth and the minimum round trip time and paces its sending to the product of the two, so it aims at keeping the pipe full without filling the queue. That is a direct answer to the two weaknesses named in section 01: it does not collapse on wireless loss that is not congestion, and it does not need a full buffer before it reacts. It is deployed on Google.com and YouTube, which is why it is a fair thing to be asked about.
The honest caveat in section 01 is real, but the link layer hides most of it. 802.11 retransmits a corrupted frame at the MAC layer, several times, before it ever gives up, so a burst of interference usually costs TCP some added delay rather than a visible loss. What TCP does see instead is a round trip time that swings wildly, which inflates the retransmission timer and makes genuine timeouts rare but slow. The classic exam answer, that TCP halves its window over Wi-Fi for no reason, is right in principle and overstated in practice, and saying both halves is the stronger answer.
QUIC runs over UDP, so its congestion control lives in the application’s own library rather than in the operating system. RFC 9002 specifies a NewReno-style controller as the default and explicitly allows others, so Cloudflare and Google ship CUBIC and BBR inside the QUIC stack itself and can change them without touching a kernel. QUIC also acknowledges explicit packet-number ranges rather than one cumulative byte offset, which lets a sender see exactly which packets were lost and removes the ambiguity that forced NewReno to exist.
07 Interview questions
What they ask, and where they push
The reliable pattern here is that they ask for the rules, you give them, and then they name a state and a signal and want the next three values. Say which phase you are in before you give a number; a bare number with no phase attached reads as memorisation, and the follow-up will catch it.
What is congestion control, and how is it different from flow control?
What is actually happening in the network when we say it is congested, and what is congestion collapse?
The network never tells the sender it is congested. So how does TCP find out?
Walk me through slow start. And if cwnd doubles every round trip, why is it called slow?
What is AIMD, and which phase of TCP is it?
You get three duplicate ACKs. What do you do, and why not wait for the timer instead?
Why does a timeout reset cwnd to 1 when three duplicate ACKs only halve it?
Tahoe or Reno, what exactly is the difference?
A sender has a congestion window and the receiver has advertised a window. How does it decide how much to send?
Two flows share one bottleneck but one has a much shorter round trip time. Who gets more, and why?
TCP was designed for wired links. What goes wrong over Wi-Fi or a mobile network?
Do you actually deal with any of this in a real job?
08 Practice problems
Six to work on paper
Wherever a problem runs over rounds, write out one line per round trip rather than jumping to the pattern. Two of the six turn on applying a rule to the cwnd that is actually in force rather than the one you remember, and one of them cannot be finished without noticing what a cumulative acknowledgement is unable to say.