How does backpressure work in reactive streams (Reactive Streams spec, Project Reactor/RxJava)?
When a fast producer meets a slow consumer, something has to give — backpressure is the protocol that lets the consumer say "not yet" instead of drowning.
LEARN BY DOINGPut your intuition to work3 steps
How does backpressure work in reactive streams (Reactive Streams spec, Project Reactor/RxJava)?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why push-based systems need a brake pedal
In a classic synchronous call, the consumer sets the pace: it calls a function, blocks, and only asks for more work when it is ready. Reactive pipelines break that guarantee by design — a Publisher pushes data asynchronously, and it can produce items faster than a Subscriber can consume them. A socket streaming market ticks, a Kafka consumer draining a hot partition, or a database cursor racing ahead of a slow downstream aggregation are all the same shape of problem: supply outruns demand, and the imbalance has to land somewhere. Without a control mechanism it becomes an unbounded buffer that eventually triggers an OutOfMemoryError, silently dropped data, or a consumer that falls further and further behind until the whole pipeline collapses.
The Reactive Streams specification — adopted almost verbatim into java.util.concurrent.Flow since Java 9, and implemented by Project Reactor, RxJava, and Akka Streams — solves this with a pull-based-over-push-based hybrid protocol built from four interfaces: Publisher<T>, Subscriber<T>, Subscription, and Processor<T,R> (a component that is both). Its single governing rule is deceptively simple: a Publisher must never deliver more elements to a Subscriber than that Subscriber has requested. Demand, not supply, drives the flow.
#The protocol, step by step
- The Subscriber calls
publisher.subscribe(subscriber). - The Publisher calls
subscriber.onSubscribe(subscription), handing over aSubscription— the only channel the Subscriber has to signal demand or cancel. - The Subscriber calls
subscription.request(n)to say "I can handle n more items right now." - The Publisher calls
onNext(item)at most n times — never more than the outstanding, unfulfilled demand. - The cycle repeats until the Subscriber calls
cancel(), or the Publisher terminates the stream withonComplete()oronError(throwable).
#When demand can't be honored
A slow consumer or a bursty producer will eventually create a moment where new items arrive and there is no outstanding demand to absorb them. Reactive libraries expose a handful of named strategies for exactly that moment:
| Strategy | Behavior on overflow | Best fit |
|---|---|---|
| BUFFER | Queue the excess (bounded or unbounded) until demand catches up | Bursty but ultimately drainable load |
| DROP | Discard the newest items that exceed demand | Occasional loss is acceptable, order of surviving items still matters |
| LATEST | Keep only the most recent unconsumed item | Live values that supersede older ones — a price ticker, a sensor reading |
| ERROR | Terminate the stream with onError | Overflow signals a bug or a hard SLA violation, not normal operation |
#Code: manual demand control with Project Reactor
Most Reactor code never touches request(n) directly — calling .subscribe(consumer) implicitly requests Long.MAX_VALUE, which is "unbounded," relying on upstream operators to still respect a slow downstream via internal buffering and operators like limitRate. But when you need to throttle a specific slow consumer yourself, BaseSubscriber gives you the raw protocol:
Flux<Long> ticks = Flux.interval(Duration.ofMillis(1)).onBackpressureBuffer(1000,dropped -> log.warn("Dropped tick: {}", dropped),BufferOverflowStrategy.DROP_OLDEST);ticks.publishOn(Schedulers.parallel()).subscribe(new BaseSubscriber<Long>() {@Overrideprotected void hookOnSubscribe(Subscription subscription) {request(10); // initial demand: "send me 10, no more"}@Overrideprotected void hookOnNext(Long tick) {processTick(tick); // slow work, e.g. a DB writerequest(1); // ask for exactly one more once ready}@Overrideprotected void hookOnError(Throwable error) {log.error("Tick stream failed", error);}});
Each call to request(1) only happens after processTick finishes, so the Publisher is structurally prevented from ever getting more than one tick ahead of the consumer. RxJava draws the same line between Observable (no backpressure support, meant for short or UI-driven sequences) and Flowable (fully backpressure-aware): Flowable.create(emitter -> { ... }, BackpressureStrategy.BUFFER) picks one of the same four outcomes — BUFFER, DROP, LATEST, ERROR — up front.
🧠 Check your understanding
0/3 · 0/3 answered1. In the Reactive Streams protocol, what is required before a Publisher may call onNext on a Subscriber?
2. Which backpressure strategy fits a live price ticker, where only the freshest value matters and stale ones are useless?
3. Why can a mouse-move stream never truly honor backpressure?