Networking · Transport Protocols

Advertised Windows and Zero-Window Probing

What the receive window field promises, how the receiver computes the value it advertises, and how a sender recovers when the peer advertises zero and then forgets to reopen.

Flow control is the part of a transport protocol that stops a fast sender from outrunning a slow receiver. It is separate from congestion control, which stops a sender from outrunning the network, and the separation is worth holding onto: the two mechanisms act on the same variable, the number of bytes in flight, but they are driven by different signals and they fail in different ways.

The receive window is the flow control mechanism in TCP. It is sixteen bits in the base header, plus a shift count from an option when window scaling is negotiated, and its meaning is a promise of buffer space.

What the field actually promises

The window field in a segment states the number of bytes, beyond the acknowledgement number, that the sender of that segment is currently willing to accept. It is not a rate, not a buffer size, and not a credit that accumulates. It is a statement about the interval [ACK, ACK + window).

Two consequences follow directly.

The window is relative to the acknowledgement number in the same header. A receiver that has accepted bytes up to RCV.NXT = 5000 and has 3000 bytes of free buffer advertises a window of 3000 with acknowledgement 5000, meaning bytes 5000 through 7999 are acceptable. A window of zero means byte 5000 itself is not acceptable. The relationship between the field and the receiver’s counter is fixed by the arithmetic in Sequence Numbers and the Sliding Window Arithmetic.

The window may change in either direction, subject to one rule: the right edge must not move backwards. The receiver may advertise less space, provided the new right edge — RCV.NXT + RCV.WND — is no lower than the previous one. RFC 9293 states this as a SHOULD NOT and separately requires the sender to tolerate a peer that shrinks anyway, because a sender that has already transmitted bytes inside the old window would otherwise find itself with data that is simultaneously in flight and out of bounds.

The reason for the constraint is that the sender has no way to un-send. It can only wait for those bytes to be acknowledged or to be declared lost.

How a receiver arrives at the number

The advertised value is free buffer space, and when a receiver sends an update it advertises all of the space that is free at that moment. What the receiver controls is when to send the update. A one-byte increase invites a one-byte segment, which spends forty of its forty-one bytes on headers, so a receiver that advertised every application read would multiply the packet count for no gain.

RFC 1122 describes the rule in terms of three quantities: the total buffer RCV.BUFF, the bytes received but not yet consumed by the application RCV.USER, and the currently advertised window RCV.WND. The right edge, RCV.NXT + RCV.WND, is held fixed while data arrives and is consumed, and is advanced only when the unadvertised space satisfies

RCV.BUFF - RCV.USER - RCV.WND >= min(RCV.BUFF / 2, MSS)

At that point the window is set to the full free space. The effect is that the window advances in steps of about one segment for realistic buffer sizes, which is what keeps it from being advertised in tiny increments.

/* Receiver-side silly-window avoidance: returns the window to advertise now,
   which is the previous value unless the unadvertised space has grown enough. */
static uint32_t window_to_advertise(uint32_t rcv_buff, uint32_t rcv_user,
                                    uint32_t adv_wnd, uint32_t mss)
{
    uint32_t free_space  = rcv_buff - rcv_user;
    uint32_t unadvertised = free_space - adv_wnd;

    if (adv_wnd == 0 && free_space > 0)
        return free_space;              /* reopen immediately: the sender is stalled */

    uint32_t threshold = rcv_buff / 2;  /* the recommended fraction */
    if (mss < threshold)
        threshold = mss;

    return (unadvertised >= threshold) ? free_space : adv_wnd;
}

The zero case is special because the sender is not merely idle, it is blocked. A sender with a zero window has nothing it is permitted to transmit, so it cannot generate the traffic that would carry an implicit window update. If the receiver waited for the half-buffer threshold before announcing that space had appeared, the sender would wait indefinitely for an update the receiver had not yet decided to send. Reopening from zero is therefore immediate.

The zero-window state and its timer

A sender that receives a zero window stops transmitting and starts a persist timer. The timer’s purpose is to generate a probe: a segment containing one byte of data at the left edge of the window, or no data at all if there is nothing to send, sent specifically to elicit an acknowledgement carrying the current window.

The probe is necessary because acknowledgements are not reliably generated for data that is not sent. If the receiver’s window update is itself lost, and window updates are not retransmitted because they are pure acknowledgements with no data to protect, then the sender has no way to learn that the window has reopened. The persist timer makes the sender ask again.

The probe interval is bounded on both sides. Repeated probes at the retransmission timeout would congest the path with segments that cannot be accepted, so the interval backs off, and a zero-window condition can persist for minutes while an application drains a buffer slowly. Backing off to the 60-second cap that the retransmission timer permits would leave the connection idle for up to a minute after space became available, so implementations bound the persist interval separately from the retransmission timeout, and the bound is a local configuration value rather than a number the specification fixes.

There is a subtlety in what the probe contains. RFC 9293 describes the probe as at least one octet of new data if any is available, otherwise as a retransmission, sent to provoke an acknowledgement that repeats the current window. The probe byte carries the first sequence number beyond the range the receiver has agreed to accept, which is by definition outside a zero window; the receiver still answers, and that answer is the point of the exchange. If space has appeared, the acknowledgement carries a nonzero window and the sender resumes; if not, it carries zero again and the timer re-arms. Should the receiver accept the byte, a later probe repeats a sequence number it has already recorded, so the duplicate is discarded rather than delivered twice.

Window scaling and the 65,535-byte ceiling

The sixteen-bit field caps the advertised window at 65,535 bytes. On a path with a 100 ms round trip, that ceiling limits a single connection to roughly 5.2 Mbit/s regardless of link capacity, because the sender cannot have more than one window in flight per round trip.

RFC 7323 lifts the ceiling with an option that carries a shift count between 0 and 14. Both sides send the option in their SYN, and the shift applies to the window field in each direction independently. The effective window is the sixteen-bit value shifted left by the negotiated count, so the maximum is 65535 × 2^14, just under 1 GiB.

The scaling factor is fixed for the connection’s lifetime. It cannot be renegotiated later, which means the value chosen at handshake time bounds the connection forever. A receiver that negotiates a large shift and then finds itself under memory pressure cannot advertise a window smaller than one step of that shift, because the field cannot express a value below 1. The granularity of the advertised window is 2^shift bytes, so a shift of 14 makes the smallest nonzero advertisement 16,384 bytes. Implementations choose the shift from the receive buffer size at connection setup, and the choice is a commitment to that much buffering per connection. A shift count above 14 that arrives in a SYN is not an error to refuse: the option must be clamped to 14, and the sender is then free to use less than the window that results.

The interaction with the sender’s own buffer

The window the sender honours is the smaller of the receiver’s advertised window and the sender’s own send buffer. A socket’s send buffer is set by the application or by the system default, and it is the binding constraint on many connections, particularly on the client side where the default is modest.

This produces a state that looks like a receiver problem but is not. An application writes 1 MiB to a socket whose send buffer is 64 KiB and whose peer advertises a 1 MiB window. The write blocks after 64 KiB because the kernel has nowhere to put the rest. The peer is not slow; the local buffer is small. Raising the advertised window does nothing, because the sender never has more than 64 KiB outstanding to be acknowledged.

The effective in-flight limit is therefore

inflight <= min(SND.WND, cwnd, buffer - unsent)

and the third term is the one applications control most directly. The congestion window is treated in The Effective Window: Congestion Window, Receive Window, and Buffer Limits.

When the receiver’s promise cannot be kept

The window is a commitment, and there are three ways a receiver fails to honour it.

The first is overcommitment. A receiver that advertises window based on free buffer without accounting for data already in flight in the network will accept more than it can store. The arriving data has nowhere to go and is dropped. The sender sees a loss and reduces its rate, which is a congestion response applied to a flow-control failure, and the resulting behaviour is a connection that runs slower than either mechanism intended.

The second is a window that is advertised but not actually available. A receiver may advertise space that the application has not yet consumed and that the kernel cannot free, because the application holds a reference to the buffer. The sender fills the window, the receiver cannot acknowledge past a point, and the connection stalls until the application releases the buffer.

The third is the lost window update. The update is an acknowledgement with no data, so it is never retransmitted. Its loss is recovered only by the persist timer, and the recovery takes at least one probe interval. This is the case the timer exists for, and it is why the timer must be armed whenever the window reaches zero rather than only when the application asks for more data.

References

  1. RFC 9293: Transmission Control Protocol (TCP) — IETF
  2. RFC 7323: TCP Extensions for High Performance — IETF
  3. RFC 1122: Requirements for Internet Hosts — Communication Layers — IETF