The idea: the receiver says how much room it has
TCP hands data to the receiving application only when the application calls recv(). Until then the data waits in the receive buffer in the kernel. If the sender kept sending while the application is slow, that buffer would overflow and the extra data would be thrown away. Flow control stops that: every ACK carries a window field, rwnd, the free space left in the receive buffer, and the sender never sends past ACK + rwnd − 1.
The page uses whole segments: 1 segment = 1 KB = 1 MSS, the application has written segments 1–20, and the receive buffer holds 8. One tick is the one-way delay, so a round trip is 2 ticks. Each tick, the segments sent last tick arrive, the receiving app reads, the receiver sends one ACK with win = free space, and the sender sends as much as its window allows.
Two windows: min(cwnd, rwnd)
The sender keeps two limits. rwnd protects the receiver and is told to it in every ACK. cwnd, the congestion window, protects the network; nobody sends it, the sender guesses it (see TCP Congestion Control). The number of unacknowledged segments may never exceed
window = min(cwnd, rwnd) may send while SND.NXT < SND.UNA + window
SND.UNA is the oldest unacknowledged segment, SND.NXT the next new one. This page keeps cwnd fixed so you can watch rwnd alone. Demo 1: the reader takes everything at once, rwnd stays 8, and cwnd = 4 is what holds the sender back: 4 segments per round trip, done at t = 10.
| rwnd (flow control) | cwnd (congestion control) | |
|---|---|---|
| protects | the receiver's buffer | the routers on the path |
| who sets it | the receiver, in every ACK | the sender, by guessing from ACKs and losses |
| shrinks when | the application reads slowly | a packet is lost or marked |
| header field | 16-bit Window (scaled) | none |
A slow reader slows the sender down
In Demo 2 the reader takes 1 segment per tick and cwnd = 8. The first 8 segments fill the buffer in one round trip, the reader frees only one slot per tick, and the ACKs say win 1, win 2 … The window is now rwnd, not cwnd, and the sender runs at exactly the reader's speed. Nothing is lost and nothing is retransmitted: the sender just waits. This is backpressure: a slow consumer at the end of a TCP connection slows the producer at the other end, with no extra protocol in the application.
Zero window and the persist timer
If the application stops reading, the buffer fills and the receiver sends ACK win 0: a zero window. The sender must stop. Later, when the application reads, the receiver sends a window update, an ACK that carries no data and only says "the window is open again".
ACKs that carry no data are not retransmitted. If the window update is lost, the receiver waits for data and the sender waits for a window: a deadlock. So a sender facing a zero window runs the persist timer. Each time it fires, it sends a zero-window probe, a tiny segment that the receiver must answer with an ACK carrying its current window. The interval starts at the retransmission timeout and doubles each time (Linux caps it at TCP_RTO_MAX, 120 s). The sender keeps probing for as long as the receiver keeps answering.
- Demo 3: the reader pauses from t = 4 to t = 11. The sender stops at t = 6 and starts the persist timer; its probe at t = 9 gets
win 0. At t = 11 the app reads all 8 segments, the window update arrives at t = 12, and sending resumes. - Demo 4: the same, but the window update is lost. The sender believes
rwnd= 0 until its next probe at t = 15 getsACK 12 win 8. Sending resumes at t = 17 instead of t = 12.
RFC 9293 lets a probe carry one byte of new data, which a receiver with room accepts. Linux sends a probe with an old sequence number (SND.UNA − 1) and no data, which any receiver must answer with a duplicate ACK; the page draws it that way.
Window size, window scaling and buffer sizes
To keep a path busy, a sender needs a full bandwidth-delay product in flight: 100 Mbit/s × 50 ms = 625 KB. The Window field in the TCP header has 16 bits, at most 65,535 bytes, so the window scale option (RFC 7323, agreed in the SYN and SYN/ACK) shifts it left by up to 14 bits, up to about 1 GB. A receive buffer smaller than the bandwidth-delay product caps throughput at buffer / RTT however fast the link is. Linux grows the buffer automatically between the limits of net.ipv4.tcp_rmem (min, default, max) unless the application fixed it with SO_RCVBUF; part of the buffer goes to kernel overhead, so the advertised window is smaller than the buffer.
Silly window syndrome
A receiver that reads 1 byte and advertises a 1-byte window, and a sender that sends 1 byte because the window allows it, end up sending 40 bytes of headers per byte of data. Both sides avoid it (RFC 1122, from Clark and Nagle): the receiver does not advertise a small opening, it waits until it can offer at least one MSS or half its buffer; the sender does not send a small segment while earlier data is unacknowledged (Nagle's algorithm). The page works in whole segments, so its receiver sends a window update once one MSS is free.
Seeing it on Linux
ss -tmishows each socket'srcv_space,snd_wnd(the peer's advertised window),cwndand the buffer memoryskmem.nstat -az | grep -i window:TcpExtTCPToZeroWindowAdv(we advertised a zero window),TCPFromZeroWindowAdv(we opened it again),TCPWantZeroWindowAdv.- In a packet capture, Wireshark marks
[TCP ZeroWindow],[TCP ZeroWindowProbe]and[TCP Window Update]. A growing Recv-Q inss -tmeans the application is not reading.
See also A TCP Socket in Linux for the send and receive buffers behind send() and recv(), ARQ: Stop-and-Wait vs Go-Back-N vs Selective Repeat for sliding windows with a fixed size, and TCP vs UDP: UDP has no flow control, so a slow reader just loses datagrams when its socket buffer is full.
What the page leaves out
Byte sequence numbers (the page counts whole segments), transmission time and a varying round trip; packet loss and retransmission of data; congestion control (cwnd is fixed here); delayed ACKs (the receiver ACKs every tick that brings data); window scaling and buffer auto-tuning; the sender's own send buffer filling and blocking send(); shrinking the window (a receiver should never move the right edge back, RFC 9293); the probe's 1 byte of data; and the timeout after which some systems give up on a peer that keeps advertising zero.