Waiting for every ack would be unusable
Imagine TCP sent one segment, waited for its ack, then sent the next. That is stop and wait, and its throughput is one segment per round trip.
segment 1460 bytes, RTT 60 ms throughput = 1460 ÷ 0.060 = 24,333 bytes per second = about 0.19 Mbps
On a gigabit link. The link is not the limit, the waiting is. The fix is pipelining: keep several segments in flight at once, and only wait when a limit is reached. That limit is the window.
in flight allowed = min(receive window, congestion window)
Two separate limits with two separate purposes, and confusing them is the most common misunderstanding in this unit.
| Flow control | Congestion control | |
|---|---|---|
| Protects | the receiver from being overrun | the network from being overloaded |
| Limit is called | receive window, rwnd | congestion window, cwnd |
| Who sets it | the receiver, in the window field | the sender, by inference |
| Signal | an explicit number in every segment | loss, delay, or an ECN mark |
| Failure without it | dropped data, wasted retransmits | queue collapse for everyone |
The sender may send only as much as the smaller of the two permits. Flow control is easy because the receiver simply says the number. Congestion control is hard because nobody tells the sender anything, so it has to guess from evidence.
Flow control, the receiver's brake
Every segment a receiver sends carries a window value, meaning "I have this many bytes of free buffer right now". The sender must keep unacknowledged data at or below it.
application is reading slowly, so the buffer fills ack 5001 win 64000 plenty of room ack 9001 win 32000 filling up ack 13001 win 8000 nearly full ack 17001 win 0 stop sending
A zero window is not an error, it is a legitimate "pause". The sender stops and sends small window probes periodically until the receiver advertises room again. Seeing zero window segments in a capture is a strong signal that the application on the receiving side is too slow, not that the network is bad.
Because the window field is only 16 bits it maxes at 65,535 bytes, far below the bandwidth delay product of a modern long path. The window scale option from Lesson 7-2 fixes this by agreeing on a shift, so a scale of 7 multiplies the advertised value by 128 and permits windows up to about 8 MB. It is negotiated only in the handshake, so if the SYN or SYN-ACK is stripped by some middlebox, the connection is stuck with 64 KB windows and mysteriously slow transfers for its whole life.
Congestion control: the sender's guess
The network gives one clear signal that it is overloaded, and that signal is loss. Routers with full queues drop packets, so a lost segment means the path is saturated. TCP therefore treats loss as a command to slow down, and its behavior is a cycle of probing upward and backing off.
Slow start. Begin with a small cwnd, typically 10 segments, and double it every round trip. Despite the name it is exponential and fast, and it is how a new connection finds the available capacity within a few round trips.
Congestion avoidance. Once cwnd passes a threshold called ssthresh, or a loss occurs, stop doubling and grow by roughly one segment per round trip instead. Careful probing rather than aggressive searching.
Fast retransmit. Recall the duplicate acks from Lesson 7-2. Three duplicate acks are treated as proof that one segment was lost while later ones arrived, so the sender retransmits that segment immediately rather than waiting for a timeout.
Fast recovery. After a fast retransmit, halve cwnd and continue from there instead of collapsing to the initial value. Only a full timeout, meaning no acks at all, causes the drastic reset to a tiny cwnd, because silence suggests something much worse than one dropped packet.
The result is a sawtooth: climb, halve, climb, halve.
| Algorithm | Growth rule | Notes |
|---|---|---|
| Reno | classic climb, halve on loss | the textbook baseline |
| CUBIC | cubic function of time since loss | Linux default, better on fast links |
| BBR | models bandwidth and RTT, ignores loss as a signal | avoids filling queues |
BBR is worth understanding conceptually, because it corrects a real flaw. Loss based control fills the router's queue until it overflows, which adds latency for everybody sharing that queue. That effect is called bufferbloat, and it is why a big upload makes an unrelated video call stutter even though bandwidth remains. BBR instead estimates the actual bottleneck bandwidth and minimum RTT and paces itself to keep the queue nearly empty.
Retransmission timers and real numbers
Fast retransmit needs later segments to arrive. When nothing arrives at all, there is only the clock, and that clock is the RTO (retransmission timeout).
- TCP measures each round trip and keeps a smoothed average plus a deviation. RTO is roughly the average plus four deviations, with a floor of around 200 ms on Linux. - Every time a retransmission of the same segment also times out, the RTO doubles. That is exponential backoff, the same idea as Lesson 3-1's Ethernet backoff. - The timestamps option lets a sender measure RTT accurately even for retransmitted segments, which fixes a genuine ambiguity: without it, an ack after a retransmission cannot be attributed to the original or the retry, so measuring the RTT from it would be wrong. That fix is known as Karn's algorithm.
Put the numbers together to see why loss hurts so much more than people expect. On a 60 ms path with a 1460 byte MSS, a healthy connection with an 8 MB window is capable of over 1 Gbps. Introduce 1 percent random loss and a loss based algorithm halves its window roughly every 100 segments, holding average throughput to a small fraction of that. This is why "the link has plenty of bandwidth" and "the transfer is slow" happily coexist, and why the first thing to check on a slow transfer is the loss counter and the retransmission count.
ss -ti dst 93.184.216.34
ESTAB 0 0 192.168.1.42:51514 93.184.216.34:443 cubic wscale:7,9 rto:264 rtt:61.4/2.1 mss:1400 cwnd:42 ssthresh:31 bytes_sent:2841204 retrans:0/14 rcv_space:64128 delivery_rate:38.4Mbps
Read it as the whole lesson in one block. The algorithm is cubic, RTT is 61.4 ms
with 2.1 ms of variance, cwnd is 42 segments, ssthresh sits at 31 which proves a
loss happened earlier, retrans:0/14 counts 14 retransmissions over the
connection's life with none outstanding right now, and delivery_rate is the
throughput actually achieved.