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 sender's byte stream: segments 8 to 11 ACKed, 12 and 13 in flight, 14 may be sent, 15 to 20 outside the window. The receiver's buffer of 8 segments holds unread segments 7 to 11 and has 3 free slots, so it sends ACK 12 win 3; the sender's right edge is 12 + 3 − 1 = 14
The receiver advertises its free space; the sender's window ends at 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)
protectsthe receiver's bufferthe routers on the path
who sets itthe receiver, in every ACKthe sender, by guessing from ACKs and losses
shrinks whenthe application reads slowlya packet is lost or marked
header field16-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.

Time-down diagram. At t = 4 the sender sends segments 10 and 11, the buffer is full and the receiver answers ACK 12 win 0. The sender stops and starts the persist timer. At t = 9 a probe gets ACK 12 win 0 and the interval doubles to 6. At t = 11 the app reads 8 segments and the window update ACK 12 win 8 is lost. At t = 15 the next probe gets ACK 12 win 8 and the sender sends segments 12 to 19
A lost window update would leave both sides waiting forever; the next probe finds the open window.
  • 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 gets ACK 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 -tmi shows each socket's rcv_space, snd_wnd (the peer's advertised window), cwnd and the buffer memory skmem.
  • 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 in ss -t means 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.