JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Flow Control and Head-of-Line Blocking

TCP keeps a fast sender from drowning a slow receiver, and its strict in-order delivery hides a subtle cost. Meet flow control, the receive window, and the head-of-line blocking that finally pushed the web onto QUIC.

Two ways a sender can go too fast

In the previous guides you saw TCP turn an unreliable network into a tidy, ordered byte stream using checksums, ACKs, sequence numbers, timers, and the sliding window. But reliability is only half the story. Even if every byte arrives correctly, a sender can still cause trouble simply by sending faster than someone can keep up. There are two very different someones to worry about, and TCP treats them as two separate problems.

The first someone is the network in the middle — the routers and links between the two hosts. If everyone blasts data at full speed, queues inside routers overflow and packets get dropped, which is the job of congestion control to prevent (you met it in the previous guide). The second someone is the receiving computer itself. The receiver pulls bytes off the connection into a buffer in memory, and an application reads from that buffer when it gets around to it. If the sender pours bytes in faster than the application drains them, the buffer fills and overflows. Protecting the receiver from the sender is the job of flow control, the subject of this guide.

The receive window: a number the receiver advertises

TCP solves flow control with a beautifully simple trick. In every segment it sends back, the receiver includes a 16-bit field called the receive window (often written rwnd). This number says, in plain terms, how many more bytes I have free buffer space for right now. The sender promises never to have more unacknowledged data in flight than this window allows. As the receiving application reads bytes out of the buffer and frees up space, the receiver advertises a bigger window; if the application stalls and the buffer fills, the advertised window shrinks toward zero.

Think of it like pouring water from a jug into a glass while a friend drinks from the glass. You watch the water level and pour only as fast as there is room. The receive window is your friend continuously telling you how full the glass is. This is TCP flow control in action: the receiver, not the sender, sets the pace, because only the receiver knows how fast its own application is draining the buffer.

Receiver buffer (say 64 KB total)

[#### already received, not yet read ####|...... free ......]
                                          ^
                                          rwnd = free space

In every ACK the receiver sends:  rwnd = (free bytes right now)
The sender's rule:                unacked bytes in flight  <=  rwnd

App reads 10 KB  -> free space grows -> next ACK advertises larger rwnd
App stalls       -> free space = 0   -> next ACK advertises rwnd = 0
The receiver advertises free buffer space; the sender never lets in-flight data exceed it.

The zero window, and how the conversation restarts

What happens when the receiver advertises a window of zero? The sender must stop. It has been told there is no room, so it parks the data and waits. But this creates a quiet danger: the receiver will eventually free up space and advertise a bigger window, yet that update travels in an ACK — and ACKs are not retransmitted. If the one segment carrying window equals 100 gets lost, the sender would wait forever for permission that never comes, and the receiver would wait forever for data it already invited. That is a deadlock.

TCP avoids the deadlock with a persist timer. When the window is zero, the sender does not simply go silent — it periodically sends a tiny one-byte probe (a window probe). The receiver answers with its current window. If the window is still zero, fine, the sender backs off and probes again later; but the moment space opens up, the next probe response carries the good news. The probe is the sender gently knocking to ask, any room yet?

Recall the round-trip time (RTT) from the reliability guide: every window update costs one round trip to take effect. That is why a fast link across a long distance — a high bandwidth-delay product path — needs a large window to stay full. The classic 16-bit window maxes out at 65,535 bytes, far too small to keep a 1 Gbps satellite-distance link busy, so modern TCP uses a window-scaling option to multiply it.

Ordered delivery has a hidden cost

Now we turn to a flaw that lives in the very thing that makes TCP so pleasant. TCP guarantees that the application reads bytes in the exact order they were sent — that is the byte stream promise. To keep it, TCP must deliver bytes strictly in sequence. So if segment number 5 is lost but 6, 7, and 8 arrive safely, the receiver cannot hand 6, 7, and 8 to the application yet. They sit in the buffer, complete and correct, but held hostage, waiting for the retransmission of 5 to fill the gap.

This is head-of-line blocking: one item stuck at the front of a line holds up everything behind it, even though those items are perfectly ready. Picture a single-file checkout queue where the person at the front cannot find their wallet. The shopper behind them has exact change in hand, but the rules say no one passes the front, so everyone waits. The delay is not caused by the network being slow for the later data — it arrived fine — it is caused by the ordering rule itself.

For a single download this is exactly what you want, and the cost is small. The pain shows up when many independent things share one TCP connection. The web is the great example. With HTTP/2, a browser fetches a page's image, script, and stylesheet over one TCP connection, interleaving them as separate logical streams. That feels clever — until a single lost packet stalls the byte stream. Because all the streams ride the same ordered pipe, the lost packet for the image freezes the script and the stylesheet too, even though their bytes already arrived. One stumble blocks everyone.

QUIC: dodging the block by moving below TCP

The fix could not come from patching TCP, because TCP's whole contract is one ordered byte stream — the blocking is baked in. So the web's designers went around it. QUIC is a newer transport protocol built on top of UDP, which gives just bare best-effort datagrams and imposes no ordering of its own. On that thin foundation, QUIC rebuilds reliability, flow control, and encryption, but with a crucial twist: it carries many independent streams that each have their own ordering and their own sequence space.

Now when a packet carrying the image is lost, only the image stream waits for its retransmission; the script and stylesheet streams are independent, so their bytes flow straight through to the application. The head-of-line block is confined to the one stream that actually lost data, instead of freezing all of them. HTTP/3 is simply HTTP carried over QUIC, which is why HTTP/3 finally escapes the transport-level blocking that limited HTTP/2.

Putting it together

Step back and the shape of the transport layer comes into focus. A sender is throttled by two ceilings at once — the receive window, which keeps it from overrunning the receiver, and the congestion window, which keeps it from overrunning the network — and it sends no faster than the smaller of the two. The receive window is the receiver talking; the congestion window is the network whispering through dropped packets. Together they let one connection share the Internet politely while never overwhelming the machine at the far end.

  1. The receiver advertises a receive window (rwnd) in every ACK, saying how much free buffer it has right now.
  2. The sender keeps unacknowledged in-flight bytes at or below min(rwnd, congestion window) — flow control and congestion control bind it together.
  3. If rwnd hits zero, the sender stops but sends periodic one-byte window probes so a lost window update can never deadlock the connection.
  4. Because TCP delivers one byte stream strictly in order, a single lost packet causes head-of-line blocking for everything behind it.
  5. QUIC, running over UDP with independent per-stream ordering, confines that block to one stream — and HTTP/3 is HTTP over QUIC.