Flow control: a slow reader slows the writer

HTTP/2 gives the receiver a window of credit measured in octets; the sender may only send DATA up to that credit and must wait for a WINDOW_UPDATE — so a slow consumer applies backpressure instead of letting the producer buffer unboundedly.

Previously

Now that calls spread across backends, picture one server-streaming RPC firing Greetings faster than the client can read them — without a brake the producer buffers until it runs out of memory, so HTTP/2 hands the reader a way to push back.

Scene 10

Flow control: a slow reader slows the writer

  1. Watch
  2. Try it
  3. Predict
  4. Capture
server · greet("A…server-streaming RPC▶ SENDINGkeeping pace · reader fastDATA FRAMES · ONE STREAMstream idleclientreading fastread 0 framesFLOW-CONTROL WINDOW · CREDIT (OCTETS)64 KiB / 64 KiBcredit availableConsumer keeps pace — window refills via WINDOW_UPDATE, DATA keeps flowing.
What to watch for

A server-streaming RPC is running: the server keeps sending Greeting responses for one greet("Ada") call, each carried in a DATA frame (the frame type from scene 4 that holds the payload bytes). The new idea is the gauge under the pipe. The receiver advertises how many bytes it is willing to accept right now — a running balance of credit measured in octets. We call this credit balance the receiver's flow-control window. Every DATA frame the server sends spends credit and drains the gauge; as the client reads frames it sends a WINDOW_UPDATE frame back to grant more credit and refill the gauge. Watch the window drain and refill while the client keeps up — DATA flows steadily because credit keeps being replenished.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Sender.sendData
spend credit per DATA frame; wait when it runs out
def sendData(stream, payload):
frame = DataFrame(payload) # only DATA is flow-controlled
# block until this stream has credit for the whole frame
while stream.window < frame.octets:
wait_for(WINDOW_UPDATE) # the backpressure point
stream.window -= frame.octets
conn.window -= frame.octets # per-connection level too
conn.write(frame)
Receiver.onData
reading frees credit and grants a WINDOW_UPDATE
def onData(stream, frame):
stream.recvBuffer.append(frame)
stream.window -= frame.octets # gauge drains
def onAppRead(stream, n_octets):
# the app consuming bytes is what frees credit
grant = WindowUpdate(stream.id, n_octets)
conn.write(grant) # refills the sender's window
Sender.onWindowUpdate
credit returns; a parked sender wakes and resumes
def onWindowUpdate(frame):
if frame.stream_id == 0:
conn.window += frame.increment # connection level
else:
stream(frame.stream_id).window += frame.increment
# any sender parked in sendData's wait loop wakes here
wake_waiters()

Where this sits in Build a gRPC-style RPC framework

Scene 10 of 14, in the Resilience act — Client-side balancing, backpressure, head-of-line blocking.. The receiver advertises a window of credit; the sender may only send DATA up to it. A slow reader stops granting credit, so the producer pauses instead of OOMing.

Up next. This keeps one stream's writer polite to its reader — but all our streams still share ONE TCP connection, and a single lost packet on that connection can freeze every stream at once, no matter how healthy their windows are.

All 14 scenes in Build a gRPC-style RPC framework · Every curriculum

Built with Arqly
Every scene in Build a gRPC-style RPC framework builds on the one before it.All 14 Build a gRPC-style RPC framework scenes