The Runtime Theory
SystemInternalsdistributed systems

Streaming Backpressure: Buffer Fill, Window Shrink, and the Slow Producer

A step-by-step walk from a fast producer to a slow consumer — the buffer that fills, the TCP window that shrinks, and the stall that propagates upstream.

The Runtime Theory Team3 min read07 steps

layer stack

System

HWHardware
KKernel
RTRuntime
APPApplication
SYSSystem
CLIClient
NETNetwork
TLSCrypto
SRVServer

adjacent altitudes in this subsystem are still being traced

trace spine

  1. 01 producer outruns consumer
  2. 02 buffer starts filling
  3. 03 buffer hits its limit
  4. 04 backpressure propagates
  5. 05 window shrinks
  6. 06 producer stalls
  7. 07 consumer catches up

Backpressure is the mechanism that keeps a fast producer from drowning a slow consumer. It sounds exotic; in practice it is a buffer filling up and the pipeline refusing to take more. Trace one slow-consumer episode through the layers.

trace stepSystem
A stream of events arrives at 1,000 msg/s. The consumer — a database batch writer, a slow transformation — processes 200 msg/s. The producer is oblivious and the pipeline is about to discover the gap the hard way.
trace stepSystem
Between producer and consumer sits a buffer: the OS socket buffer on the consumer's side (receive queue, default 128KB–6MB on Linux), an in-process channel (Go chan with capacity 1,000), or a broker partition. Events pile up at 800 msg/s net. This buffer is the only thing standing between the producer and reality — and it is finite.
trace stepSystem
At 100 bytes per event, the 128KB socket buffer fills in ~1.6 seconds; the 1,000-slot channel in ~1.25 seconds. The buffer is now full. Every additional event has nowhere to go. This is the moment backpressure starts — not as a design decision, but as a physical fact: a full buffer must refuse.
trace stepSystem
The refusal travels upstream. TCP: the consumer's kernel stops ACKing more than the window, and the producer's kernel send buffer fills in turn; the producer's write() syscall blocks. In-process: the channel's send blocks the producer goroutine. Reactive Streams: the subscriber's outstanding-demand counter hits zero, and the publisher's onNext must wait. The consumer's slowness has become a stall, stage by stage, all the way to the source.
trace stepSystem
Concretely, on TCP: the consumer's advertised receive window (win=) drops from 128KB toward zero as its buffer fills; the producer's congestion window and send rate collapse to the consumer's consumption rate. What was 1,000 msg/s of sending becomes exactly 200 msg/s — the slow consumer's rate, imposed mechanically. You can watch it: tcpdump shows the window collapsing before the stalls.
trace stepSystem
The producer's write() or send() now blocks: the pipeline has become as slow as its slowest stage, by construction. The producer's own buffer (its outbound queue) fills, and if the producer is a user-facing service, its request handlers block and requests queue — the stall propagates all the way to the user's request, which is where backpressure eventually always lands. Nothing in a pipeline can run faster than the slowest consumer indefinitely.
trace stepSystem
The consumer's batch completes, its buffer drains, the window opens again, the producer's writes unblock. The system returns to its natural rate — the consumer's rate. The episode is over; the mechanism is silent again. The buffer size decides only how long the pipeline masks the mismatch before the stall — a 6MB socket buffer buys ~50 seconds of masking at this rate; a 1,000-slot channel buys ~1.25 seconds.
text
producer ──write()──▶ [socket buffer] ──▶ consumer (200 msg/s)
    ▲                     │ full?
    └── blocked by TCP window ─┘  →  producer rate == consumer rate

The economics: backpressure converts a rate mismatch into a stall with zero data loss and zero OOM, at the cost of latency and head-of-line blocking. Its beauty is that it is everywhere already — TCP flow control is the oldest backpressure on the internet, and every framework that blocks on a full buffer is reimplementing the same idea: the pipeline's speed is set by its slowest stage, and the buffers only decide how politely everyone finds that out.