Backpressure
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.

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.

The first sentence of the official introduction was captured on October 28, 2026. It states the initiative's goal; the individual protocol rules are specified in the document linked below. Specification and MIT-0 license.
A Publisher emits elements, a Subscriber receives them, and a Subscription connects demand and cancellation. The Subscriber uses request(n) to add the number of elements it is prepared to receive. Publisher rule 1.1 limits cumulative delivery to cumulative demand, while Subscription rule 3.8 makes each new request additive. Reactive Streams 1.0.4 rules 1.1 and 3.8.
After request(2) and delivery of one element, one unit of demand remains. Calling request(3) then raises the remaining demand to four. These are illustrative counts, not a limit of four elements per second. Demand permits delivery; it does not require a source to produce that many elements if it has no data or the stream ends.
Java 9's java.util.concurrent.Flow provides Publisher, Subscriber, Subscription, and Processor interfaces. Its documentation describes them as corresponding to Reactive Streams. A shared contract matters because agreeing only on a method that sends data leaves demand, cancellation, and termination disconnected. Java 9 Flow, Subscription demand and cancellation.
Different ways to express waiting
Mechanism | How downstream constraints travel upstream | Relevant scope |
|---|---|---|
Synchronous calls or blocking writes | The sender waits for the call to return | Waiting may occupy a thread |
Bounded asynchronous queue | A full queue suspends the producer's | The producer must await completion |
Pull | Production proceeds when the consumer asks for the next value | Prefetching and internal queues need their own bounds |
Demand-based streams | Elements are delivered within remaining demand | Element count differs from processing completion and bytes |
- Mechanism
Synchronous calls or blocking writes
- How downstream constraints travel upstream
The sender waits for the call to return
- Relevant scope
Waiting may occupy a thread
- Mechanism
Bounded asynchronous queue
- How downstream constraints travel upstream
A full queue suspends the producer's
put- Relevant scope
The producer must await completion
- Mechanism
Pull
- How downstream constraints travel upstream
Production proceeds when the consumer asks for the next value
- Relevant scope
Prefetching and internal queues need their own bounds
- Mechanism
Demand-based streams
- How downstream constraints travel upstream
Elements are delivered within remaining demand
- Relevant scope
Element count differs from processing completion and bytes
The common connection is that a downstream constraint changes upstream behavior. A blocking implementation may wait on a thread; an asynchronous one may postpone resuming a task while allowing other work to run. Reactive Streams requires non-blocking signaling so that the control path between asynchronous stages does not hold threads waiting for one another. Reactive Streams problem and scope.
Waiting can therefore be represented as demand or a condition for resumption. A separate contract determines when business processing is complete. Treating receipt of an element as completion of its database write would obscure how much work is still running.
A small example with two queue slots
This standalone example requires Python 3.11 or later. A producer puts items into a queue with maxsize=2 and awaits each put. When the queue is full, await queue.put() suspends that coroutine while the event loop can run other work, including the consumer. asyncio.Queue.
import asyncio
async def produce(queue):
for item in range(6):
await queue.put(item)
await queue.put(None)
async def consume(queue):
values = []
while True:
item = await queue.get()
if item is None:
return values
await asyncio.sleep(0.01)
values.append(item)
async def main():
queue = asyncio.Queue(maxsize=2)
async with asyncio.TaskGroup() as group:
group.create_task(produce(queue))
consumer = group.create_task(consume(queue))
assert consumer.result() == list(range(6))
print(consumer.result()) # [0, 1, 2, 3, 4, 5]
if __name__ == "__main__":
asyncio.run(main())Here None is a termination marker excluded from the input values. The single consumer processes the preceding items in order before receiving it, and the parent waits for both tasks when leaving the TaskGroup. The flow does not need the separate unfinished-item accounting provided by join and task_done, so they are omitted.
If the consumer fails with an ordinary exception, TaskGroup also cancels the producer. Cancelling the individual consumer is treated differently from this failure rule, so stopping both tasks requires a path that ends their shared lifetime, such as cancellation of the parent. This example demonstrates queue waiting rather than the complete Reactive Streams protocol. TaskGroup failure and cancellation rules.
Why buffers and rate limits need separate names
Mechanism | What it directly controls | Relationship to backpressure |
|---|---|---|
Buffer bound | How much data may wait | A full buffer must influence upstream behavior to change the flow |
Rate limiter | How much is allowed in a period | A fixed allowance may differ from current consumer capacity |
Concurrency limit | How many jobs run at once | An unbounded waiting queue still permits accumulation |
Dropping or rejecting | What is discarded or not accepted | A separate policy is needed where production cannot be slowed |
- Mechanism
Buffer bound
- What it directly controls
How much data may wait
- Relationship to backpressure
A full buffer must influence upstream behavior to change the flow
- Mechanism
Rate limiter
- What it directly controls
How much is allowed in a period
- Relationship to backpressure
A fixed allowance may differ from current consumer capacity
- Mechanism
Concurrency limit
- What it directly controls
How many jobs run at once
- Relationship to backpressure
An unbounded waiting queue still permits accumulation
- Mechanism
Dropping or rejecting
- What it directly controls
What is discarded or not accepted
- Relationship to backpressure
A separate policy is needed where production cannot be slowed
The queue's two slots bound waiting items. An item already being processed, the producer's next item, and queues in other stages exist outside those slots. This differs from bounding total process memory in bytes. The example has one producer and one consumer so that the path remains visible.
Some inputs, such as sensor readings or mouse movements, originate at a rate we cannot control. A stream boundary may buffer, coalesce, or drop them, but preserving every original event then needs a separate guarantee. Reactive Streams likewise discusses buffering or dropping to respect demand bounds when the original source cannot be slowed. Subscriber-controlled queue bounds.
TCP, queues, and streams express backpressure through different mechanisms for communicating capacity between stages. Their history connects byte-level flow control, backlog between asynchronous stages, and shared demand protocols. Reading an implementation means following both the data path and the path through which constraints return upstream.