Backpressure in Project Reactor, explained with code
What request(n) really means, how prefetch and limitRate shape demand, and how to choose an overflow strategy when a producer is faster than its consumer.
Reactive Streams has one idea that sets it apart from “just callbacks”: the consumer controls the speed. A subscriber says how many elements it can handle, and a well-behaved publisher never sends more. That’s backpressure.
In Project Reactor this happens mostly out of sight. Understanding it is the difference between a pipeline that stays
calm under load and one that ends with an OutOfMemoryError or an OverflowException.
Demand is a number
The protocol is tiny:
public interface Subscription {
void request(long n); // "I can take n more"
void cancel();
}
A Subscriber gets a Subscription in onSubscribe and calls request(n). The publisher may then call onNext at
most n times. Long.MAX_VALUE means “unbounded, just push everything”.
When you call .subscribe(System.out::println), Reactor requests Long.MAX_VALUE. That’s convenient, but it means
no backpressure at the final subscriber. To see real demand signalling, write your own subscriber:
Flux.range(1, 10)
.log()
.subscribe(new BaseSubscriber<Integer>() {
@Override
protected void hookOnSubscribe(Subscription s) {
request(1);
}
@Override
protected void hookOnNext(Integer value) {
process(value);
request(1); // ask for the next one only when ready
}
});
With .log() you’ll see request(1) / onNext alternate, so the producer is paced by the consumer.
Operators have queues: prefetch
Operators that switch threads or merge streams, such as publishOn, flatMap and concatMap, keep an internal queue.
They don’t forward your demand 1:1. They prefetch a batch from upstream and refill it as it drains.
Flux.range(1, 1_000)
.log("upstream")
.publishOn(Schedulers.boundedElastic()) // default prefetch: 256
.subscribe(this::slowWork);
In the log you’ll see request(256) upstream, followed later by request(192). By default Reactor replenishes when
75 % of the prefetched elements have been consumed. You can tune the prefetch:
.publishOn(Schedulers.boundedElastic(), 32)
limitRate: shaping demand explicitly
limitRate(n) caps the demand that goes upstream, whatever the downstream asks for. That even includes
Long.MAX_VALUE:
repository.findAll() // e.g. a database cursor
.limitRate(100) // fetch in pages of 100, refill at 75
.map(this::toDto)
.subscribe(sink::write);
This is the easiest way to turn an “unbounded” subscriber into well-sized batches against a database, a message broker or an HTTP API that supports pagination.
When the producer can’t slow down
Some sources can’t be paused: UI events, sensor data, a WebSocket you don’t control. They’re hot. For these you have to decide what happens when demand runs out:
| Strategy | Behaviour | Good for |
|---|---|---|
onBackpressureBuffer(n) | Queue up to n items, then error (or a custom handler) | Short bursts |
onBackpressureDrop() | Discard items no one asked for | Metrics, telemetry |
onBackpressureLatest() | Keep only the newest item | Prices, positions, UI state |
onBackpressureError() | Fail fast with OverflowException | When loss is never acceptable |
priceTicks() // hot, can't slow down
.onBackpressureLatest() // a stale price is useless anyway
.publishOn(Schedulers.parallel(), 1)
.subscribe(this::render);
When you bridge callback APIs with Flux.create, you choose the strategy up front:
Flux.<Event>create(sink -> listener.onEvent(sink::next), FluxSink.OverflowStrategy.DROP);
Flux.create defaults to BUFFER, which is unbounded. It’s a common source of memory leaks when the consumer is
slower than expected.
Practical checklist
- Know where your unbounded requests are.
subscribe(consumer),block*()andcollectList()all request everything. - Bound concurrency in
flatMap.flatMap(fn, 8)limits how many inner publishers run at once. That’s backpressure on work, not just elements. - Pick an overflow strategy on purpose for every hot source and every
Flux.create. - Use
.log()while developing. Watchingrequest(n)lines is the fastest way to understand a pipeline.
Backpressure isn’t something you add at the end. It’s a property of every link in the chain, and Reactor gives you a knob for each of them.
This article lives on GitHub as plain Markdown. Spotted a typo or want to talk about it?