βSuppose a file reader produces 100 items per second while the next stage processes 60. For as long as that difference persists, another 40 items accumulate each second. A larger buffer buys time, but it eventually fills too.
Backpressure is a way for the receiving side's capacity to influence production or transmission upstream. Data travels from producer to consumer, while information about how much more can be accepted travels in the other direction. The problem is how processing stages with finite resources coordinate their rates.
Telling the sender what the receiver can accept
This problem predates asynchronous stream libraries. The 1981 TCP specification, RFC 793, describes a receiver advertising a window: the number of bytes it is currently prepared to receive. A sender considers that capacity even when it has more data to send. This is an early protocol example of communicating downstream constraints, rather than evidence that the term backpressure originated with TCP. RFC 793 section 2.6.
TCP counts bytes; an application queue may count items, and a stream API may count requested elements. They address related problems at different layers. Available TCP receive-buffer space does not mean that order processing or file conversion has finished, so an application can still accumulate work in an unbounded queue above TCP.
Flow control and congestion control have different subjects. Flow control considers the receiver's capacity; congestion control considers conditions along the network path. The current TCP base specification, RFC 9293, distinguishes the send window from the congestion window. RFC 9293 sections 3.8.2 and 3.8.6.
Where waiting goes in an asynchronous pipeline
Sequential function calls on one thread naturally couple progress: the next call waits for the current one to return. Moving work into a queue or separate task lets the producer advance without waiting for the consumer to finish. We then have to decide where the intervening backlog lives and how large it may grow.
Asynchronous execution and bounded resource use are separate design decisions. A queue that keeps accepting items when its consumer slows down separates the two executions, but it does not coordinate their rates. A capacity limit needs a feedback path so that waiting affects upstream production.
[Image]
The upper arrows show data flow; the lower path shows waiting or demand information travelling upstream. This is an explanatory diagram, rather than a library class structure or a network packet format.
If a producer waits for each queue put to complete, a full queue constrains the producer. If it keeps creating separate tasks to perform those puts, the queue can stay small while waiting tasks accumulate outside it. We need to trace which stage actually stops further production.
Demand becomes a shared library contract
Reactive Streams defines interfaces and rules through which asynchronous stream implementations can exchange these signals. The commit referenced by its 1.0.0 tag dates to April 2015, and the official announcement describes the goal of standardizing asynchronous streams with non-blocking backpressure. The scope extends from managing one queue to preserving flow-control rules when different components are connected. 1.0.0 record, official announcement.
[Image]
+Suppose a file reader produces 100 items per second while the next stage processes 60. As long as that difference persists, 40 more items accumulate each second. A larger buffer can hold them for a while, but it too will fill if the consumer continues at the same rate.
With backpressure, the sender paces production or transmission according to the receiver's available capacity. Data flows downstream from producer to consumer, while signals indicating how much more can be accepted travel upstream. TCP receive windows, waiting on an asynchronous queue, and stream demand address this problem in different units and execution models.
The receiver advertises how much it can accept
In RFC 793, the 1981 TCP specification, a receiver advertises a window stating how many bytes it can currently accept. The sender takes that limit into account even if it has more data ready. Protocols communicated receiver capacity to senders before asynchronous stream libraries appeared.[Footnote 1]
When the receiving TCP stack accepts data, the application may still have an order to process or a file to convert. If the application keeps reading from the socket into an unbounded queue, work can accumulate internally even as it frees the TCP receive buffer. The producer and consumer beyond the socket need their own connection between downstream capacity and upstream waiting.
Networking also distinguishes flow control from congestion control. A receiver advertises its own capacity; the sender's congestion control governs what the network path can carry. RFC 9293, the current TCP base specification, distinguishes the send window from the congestion window.[Footnote 2]
In an asynchronous pipeline, where does waiting occur?
When one thread calls functions in sequence, it waits for a call to finish before making the next one. Move work to a queue or a separate Task, and a producer can create more work without waiting for the consumer to finish. The system then needs a place and a limit for data waiting between them.
If the producer awaits every queue put, a full queue prevents it from adding the next item until a slot opens. If instead it keeps creating Tasks to perform each put, the queue can remain small while Tasks waiting outside it accumulate.
[Image]
The upper arrows show data flow, and the lower path shows waiting or demand signals traveling upstream. Whether through queue waiting or explicit demand, that feedback governs the producer's next action.
A server uses queues between stages as control points
In their 2001 SEDA paper, Welsh, Culler, and Brewer divided a server into processing stages and made the queues between them explicit. Each stage could inspect its backlog and adjust its thread count or processing batch.[Footnote 3]
[Image]
(Original source)
Events pass through the incoming queue to a handler, which sends events to other stages. The controller adjusts resource use and scheduling in this stage.
In Β§3.3 the authors explain that a queue may reject new items, allowing an application to wait at a full queue or drop events. Placing a queue between stages does not set bounds automatically; the application uses that control point to slow production or shed load.
Separate stages are easier to tune individually, but a slow stage also needs a way to affect the stages ahead of it. The problem seen in socket flow control reappears inside the server.
Demand passed between libraries
The Reactive Streams participants defined interfaces so asynchronous stream libraries could exchange demand and cancellation under common rules. The commit tagged 1.0.0 dates from April 2015, and the official announcement states their goal of standardizing asynchronous stream processing with non-blocking backpressure.[Footnote 4]
[Image]