Loading learning content...
In the previous page, we established that flow control exists to prevent receiver buffer overflow. But a critical question remains: who decides the transmission rate? In TCP's design, the answer is unequivocal—the receiver dictates how fast the sender may transmit.
This receiver-centric architecture is not an arbitrary design choice but a principled decision rooted in information asymmetry. Only the receiver knows its own buffer state, processing capacity, and application consumption rate. The sender, isolated on the other side of the network, has no direct visibility into these conditions. TCP's elegance lies in letting the party with the best information make the decision.
This page examines receiver-based control in depth: how it works, why it's designed this way, and the precise mechanisms by which receivers communicate their capacity to senders.
By the end of this page, you will understand: (1) Why receivers are positioned to control flow, not senders, (2) The information asymmetry that drives this design, (3) How receivers actively participate in rate limiting, (4) The receiver's role in both steady-state and edge-case scenarios, and (5) The advantages and potential drawbacks of receiver-based control.
The fundamental reason TCP flow control is receiver-based lies in information asymmetry—the receiver possesses critical knowledge that the sender cannot obtain.
What the Receiver Knows
The receiver has perfect, real-time knowledge of:
What the Sender Cannot Know
The sender, by contrast, is fundamentally limited:
This asymmetry leads to a clear design principle: the party with the information should make the decision.
Think of sender and receiver like a shipping company and a warehouse. The shipping company (sender) can move packages quickly, but only the warehouse (receiver) knows how much space is available. The optimal strategy is for the warehouse to tell the shipping company 'I can accept X more packages' rather than the shipping company guessing warehouse capacity.
Why Not Sender-Based Control?
One might wonder: why not have the sender estimate receiver capacity and self-regulate? This approach fails for several reasons:
Sender-based flow control would require the sender to guess what only the receiver knows—a fundamentally flawed approach.
In TCP's receiver-based flow control, the receiver is not a passive data sink but an active participant in controlling the data flow. This active role manifests in several ways:
1. Window Advertisement
The receiver calculates and advertises its available buffer space in every ACK segment. This 16-bit field (or 30-bit with window scaling) tells the sender exactly how much more data the receiver can accept.
2. Dynamic Adjustment
The receiver continuously recalculates the window based on:
3. Rate Governance
By controlling the window size, the receiver effectively controls the sender's maximum transmission rate. A smaller window constrains the sender; a larger window permits faster transmission.
4. Pause and Resume
The receiver can pause transmission entirely by advertising a zero window, then resume by opening the window when ready. This gives the receiver complete control over data flow timing.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
# TCP Receiver: Active Flow Control Participation class TCPReceiver: """ Models the receiver's active role in TCP flow control. The receiver is not a passive data sink but actively governs the sender's transmission rate through window advertisement. """ def __init__(self, buffer_size: int): self.rcv_buffer_size = buffer_size # Total buffer capacity self.rcv_buffer = bytearray() # Actual buffered data self.last_byte_rcvd = 0 # Highest sequence received self.last_byte_read = 0 # Highest sequence read by app self.next_expected_seq = 0 # Expected next byte def compute_window(self) -> int: """ Calculate the receiver window (rwnd). This is called before every ACK is sent to ensure the advertised window reflects the current buffer state. Returns: Available buffer space in bytes """ buffered_data = self.last_byte_rcvd - self.last_byte_read available_space = self.rcv_buffer_size - buffered_data # Window cannot be negative return max(0, available_space) def receive_segment(self, seq_num: int, data: bytes) -> dict: """ Process an incoming segment and generate ACK with window. The receiver actively participates by: 1. Buffering the data 2. Updating state variables 3. Computing the new window 4. Generating ACK with current window advertisement """ # Validate sequence number (simplified) if seq_num != self.next_expected_seq: # Out-of-order handling (simplified) pass # Buffer the data self.rcv_buffer.extend(data) self.last_byte_rcvd = seq_num + len(data) self.next_expected_seq = self.last_byte_rcvd # Compute current window for advertisement current_window = self.compute_window() # Generate ACK with window advertisement return { 'ack_number': self.next_expected_seq, 'window': current_window, # The critical flow control field 'flags': {'ACK': True} } def application_read(self, num_bytes: int) -> bytes: """ Application reads data from the buffer. This INCREASES the window because it frees buffer space. The receiver may send a window update if window was zero. """ # Extract data for application data = bytes(self.rcv_buffer[:num_bytes]) self.rcv_buffer = self.rcv_buffer[num_bytes:] self.last_byte_read += num_bytes # Window now increases - sender can send more new_window = self.compute_window() # If window was zero and now non-zero, send window update # (This is the "persist" response - covered later) return data def should_send_window_update(self, old_window: int) -> bool: """ Determine if a window update ACK should be proactively sent. RFC 1122 recommends sending when window increases by: - Either one full segment size, or - Half the receive buffer size This prevents the sender from stalling unnecessarily. """ new_window = self.compute_window() window_increase = new_window - old_window # Send update if window increased significantly mss = 1460 # Typical MSS half_buffer = self.rcv_buffer_size // 2 return window_increase >= min(mss, half_buffer)The code above illustrates how the receiver actively computes and advertises its window. Every incoming segment triggers a window recalculation, and every ACK carries the current window advertisement. The receiver is not passively accepting data—it is actively governing the flow.
The window advertisement is the communication channel through which the receiver exercises control. Understanding its mechanics is essential:
Location in TCP Header
The window field occupies 16 bits (bytes 14-15) of the TCP header. With optional window scaling (discussed later), this expands to effectively 30 bits. The field specifies the number of bytes the receiver can accept, relative to the acknowledgment number.
Semantics of the Window Value
When the receiver advertises a window of W bytes with acknowledgment number A, it means:
"I have successfully received all bytes up to A-1. I can accept bytes A through A+W-1."
This defines a window of acceptable sequence numbers: [A, A+W-1]. The sender must not send bytes with sequence numbers at or beyond A+W.
Piggybacking on ACKs
Every TCP segment with the ACK flag set carries a window advertisement. This piggybacking is efficient—no additional overhead is required for flow control. The window field is always present; it simply reflects current receiver capacity.
The Window as a Sliding Constraint
The advertised window creates a sliding constraint on the sender. As acknowledgments arrive, the left edge of the window advances (bytes are confirmed received). As window advertisements change, the right edge moves (more or less space is advertised).
The sender tracks two pointers:
Critically, the right edge does not always move forward. If the receiver's window shrinks (less space available), the right edge may stay fixed or even appear to move backward. TCP senders must handle this "window shrinking" gracefully.
RFC 1122 discourages receivers from shrinking the window such that the right edge moves left—this can cause the sender to have already-transmitted data suddenly be 'outside' the window. Well-behaved receivers avoid shrinking the window below outstanding data. This is related to silly window syndrome prevention, covered later.
The receiver's flow control behavior can be modeled as a state machine driven by two event types: data arrival (network events) and application reads (local events).
States
Transitions
Event Handling in Each State
FLOWING State
THROTTLED State
ZERO_WINDOW State
OPENING State
The receiver bears several specific responsibilities in TCP flow control. These are not merely suggestions—they are protocol requirements that ensure interoperability and prevent pathological behavior.
Responsibility 1: Accurate Window Reporting
The receiver MUST report an accurate window. Advertising more space than available leads to buffer overflow and data loss. Advertising less space than available artificially throttles throughput.
Responsibility 2: Timely Window Updates
When the application reads data and window opens, the receiver SHOULD send a window update promptly. Delays leave the sender unnecessarily stalled. RFC 1122 specifies: send an update when window increases by at least one MSS or half the buffer, whichever is smaller.
Responsibility 3: Avoiding Silly Window Syndrome
The receiver SHOULD NOT advertise tiny window increases that provoke the sender to send tiny segments. This "silly window syndrome" wastes bandwidth on header overhead. The receiver should wait until a reasonable window is available before advertising.
Responsibility 4: Responding to Persist Probes
When the receiver has advertised a zero window, the sender will periodically send persist probes. The receiver MUST respond to these probes with its current window status, even if the window is still zero.
Responsibility 5: Handling Out-of-Order Segments
Out-of-order segments consume buffer space (if buffered for reassembly) but cannot be delivered to the application until gaps are filled. The window calculation must account for this buffered out-of-order data.
| Responsibility | RFC Source | Requirement Level | Consequence of Violation |
|---|---|---|---|
| Accurate window reporting | RFC 793 | MUST | Data loss from overflow or throughput loss from under-reporting |
| Timely window updates | RFC 1122 | SHOULD | Sender stalls unnecessarily, throughput degradation |
| SWS avoidance | RFC 1122 | SHOULD | Bandwidth wasted on tiny segments, efficiency collapse |
| Persist probe response | RFC 793 | MUST | Sender cannot determine when window opens, connection hangs |
| Out-of-order handling | RFC 793 | MUST | Reassembly failures, reliability violations |
In RFC terminology, MUST indicates an absolute requirement for compliance. SHOULD indicates a strong recommendation that may be ignored only with good reason. Violating MUST requirements breaks protocol guarantees; violating SHOULD recommendations degrades performance but maintains correctness.
TCP's receiver-based flow control design offers significant advantages over alternative approaches:
Advantage 1: Perfect Information
The receiver has perfect knowledge of its own state. There is no estimation, no guessing, no inference from indirect signals. The receiver knows exactly how many bytes it can accept because it knows exactly how many bytes it has buffered.
Advantage 2: Real-Time Responsiveness
As the application reads data, the receiver can immediately increase its window advertisement. As memory pressure increases, the receiver can immediately decrease it. This responsiveness is limited only by ACK transmission delays, not by feedback loops.
Advantage 3: Decoupled Sender Logic
The sender's job becomes simple: don't exceed the advertised window. The sender doesn't need complex algorithms to estimate receiver capacity—it just follows the receiver's instructions. This simplifies sender implementation and reduces corner cases.
Advantage 4: Heterogeneous Network Support
Receiver-based control works regardless of network topology, latency, or bandwidth. Whether the receiver is across a local network or across the planet, the same mechanism applies. The receiver advertises based on its capacity, not on network characteristics.
Advantage 5: Application-Aware Pacing
The receiver's window naturally reflects application behavior. A paused video player causes the window to shrink; a resumed player causes it to grow. The flow control mechanism automatically adapts to application-level events without explicit coordination.
While receiver-based control is elegant, it is not without challenges. Understanding these edge cases is essential for protocol implementers and network engineers:
Challenge 1: Stale Window Information
The sender acts on window advertisements that may be stale by the time the sender receives them—at least one-half RTT in the past. In high-latency networks, the receiver's state may have changed significantly.
Mitigation: Receivers should not shrink the right edge of the window. Increasing window is safe even with stale info (receiver can handle more data); decreasing is risky.
Challenge 2: Lost Window Updates
If a window update ACK is lost, the sender may remain stalled indefinitely, believing the window is still zero.
Mitigation: The sender's persist timer periodically probes a zero-window receiver. The receiver responds with current window, ensuring eventual recovery.
Challenge 3: Malicious Receivers
A malicious receiver could advertise a large window, accept data, then discard it without acknowledging. This wastes sender resources.
Mitigation: This is a form of denial-of-service. Senders typically trust receivers; explicit mitigation requires application-layer authentication.
Challenge 4: Memory Exhaustion Attacks
An attacker could open many connections, each advertising a large window, exhausting the sender's send buffer memory.
Mitigation: Senders should limit total memory across connections. Per-connection buffer limits prevent single connections from dominating resources.
When a receiver advertises a zero window, the sender stops transmitting. But what if the subsequent window update is lost? The sender would wait forever. TCP's persist timer prevents this: the sender periodically sends tiny 'persist probes' to trigger an ACK with the current window status. This breaks the deadlock.
| Edge Case | Cause | Impact | Mitigation |
|---|---|---|---|
| Stale window | Network latency | Sender acts on outdated info | Don't shrink right edge; persist probes |
| Lost window update | Network packet loss | Sender stalls indefinitely | Persist timer probes receiver |
| Window shrinking | Receiver memory pressure | Data in flight exceeds new window | RFC discourages; sender handles gracefully |
| Slow receiver app | Application processing delay | Window shrinks to zero | Zero window handled by persist mechanism |
| Rapid window changes | Bursty application reads | Sender sees oscillating window | Sender uses latest ACK; avoids bursts |
We have examined why TCP flow control is inherently receiver-driven and how receivers actively participate in governing transmission rates. Let us consolidate the key insights:
Looking Ahead
Now that we understand the receiver's central role in flow control, the next page will examine the specific mechanism used to communicate capacity: window advertisement. We will explore the window field in the TCP header, window scaling for high-bandwidth networks, and the precise semantics of window updates.
You now understand that TCP flow control is fundamentally receiver-driven. The receiver possesses the information necessary to make flow control decisions, and it exercises control through window advertisements that the sender must respect. This receiver-centric design is a cornerstone of TCP's reliability guarantees.