Skip to content
About
Main content

Backpressure

How does a consumer slow its producer, and why does backpressure travel until something absorbs it or breaks?

The difference from , stated once and precisely:

Rate limiting is open-loop: it does not look at the result. is closed-loop: it reacts to what is really happening. Backpressure adapts to real capacity, including capacity you did not predict: a garbage-collection pause, a noisy neighbor on the same machine, a downstream that became slower. That is why backpressure is the mechanism of choice inside a pipeline you control, and rate limiting is the mechanism of choice at a boundary you do not control.

Three implementations

Reactive pull. The asks for work. A busy consumer does not ask. Here backpressure is not a mechanism at all. It is a structural consequence . This is why pull-based transports need no backpressure protocol. Examples are Kafka consumers, Volcano-model iterators in query engines, and Flow.Subscription.request(n) in Java's reactive streams. It is the simplest correct answer whenever you control both ends.

Credit-based flow control. The receiver gives the sender N units of credit. The sender may send N units and no more. The receiver gives credit back as it processes. HTTP/2, gRPC, Flink's network stack, InfiniBand and RabbitMQ's prefetch all work this way.

It is better than pure pull in one specific way: it pipelines. The sender can transmit continuously up to its credit, without waiting a for each item. You get flow control and full use of the bandwidth. Plain request-and-response pull wastes one RTT of idle time per fetch. Credit is what you use when the RTT matters.

The credit window is a buffer, so everything about buffers applies. Too small, and the link is under-used. Too large, and returns.

TCP-style windows. The receiver announces a window equal to its free buffer space (the , rwnd). The sender's in-flight bytes cannot exceed it. This is credit at the byte level, with one important addition: also keeps a (cwnd), where it infers the capacity of the shared network path from packet loss and delay. Two windows, two different scarce resources: the sender may have min(rwnd, cwnd) bytes in flight.

This distinction is worth carrying everywhere: the receiver's capacity and the path's capacity are separate limits. A healthy consumer behind a saturated network needs a different response from a saturated consumer behind a healthy network. Systems that mix the two behave badly in exactly one of the two cases.

Propagation: what makes backpressure powerful and dangerous

Backpressure is transitive (it passes from stage to stage). Stage D slows, so C's writes block, so C stops reading, so B blocks, so A blocks. The whole pipeline regulates itself to the speed of its slowest stage, with no central coordinator. That is the beautiful part, and it is why Flink and Reactive Streams are built on it.

Now the dangerous part, which is the thing to take away:

Three examples:

  • CDC (change data capture, reading a database's change log). The consumer stalls, so the replication slot (the database's bookmark for a consumer of its log) stops moving. The database keeps the (write-ahead log, the file where it writes every change before applying it) because the slot still needs it. The WAL grows on the production OLTP database (the database that handles live transactions), the disk fills, and the source database goes down. Your analytics pipeline has killed the transactional system.
  • Synchronous request chains. Backpressure travels to a user. A user cannot be backpressured and will , which adds load. Backpressure into an uncontrolled source becomes amplification.
  • Ingestion from external partners. You cannot slow them down. They send anyway. Either you drop the data or they do. Backpressure works only when the upstream can and will respond to it.

So here is the rule: backpressure requires a controllable, patient upstream. Where the upstream is a person, a partner, a fleet of devices, or a database that suffers from being paused, backpressure is not available. You must use , or a instead (all below). The most common architecture error in this whole article is to apply backpressure across a boundary where it flows into something that breaks under it.

The general defense is to end backpressure at a deliberate : a durable buffer near the ingestion edge, sized in hours, whose is shedding rather than blocking. This turns an unbounded propagation upstream into a bounded local decision.