Mastering Backpressure in Spring WebFlux for Streaming APIs
How to Handle High-Throughput Streaming Scenarios Without Overwhelming Consumers
Reactive systems excel at handling streaming data, but without careful backpressure management, you risk overwhelming downstream consumers, running out of memory, or introducing unacceptable latency.
Spring WebFlux, part of the Spring ecosystem since version 5, is built on Project Reactor, which natively supports reactive streams and backpressure-aware processing. This guide shows you how to:
- Understand how backpressure works in WebFlux and Reactor
- Control data flow in high-throughput streaming APIs
- Use practical patterns to avoid buffer overflows and slow subscribers
- Implement end-to-end backpressure handling with code examples
Let’s learn how to build resilient reactive pipelines.
Why Backpressure Matters
In classic synchronous APIs, the server blocks until the client is ready. In streaming APIs, producers may generate data faster than consumers can process it.
Backpressure provides a mechanism to signal:
“Please slow down—I’m not ready for more data yet.”
If you ignore backpressure:
- Buffers fill up
- Latency spikes
- Out-of-memory errors occur
Proper backpressure handling ensures your application remains responsive under load.
Prerequisites
- Java 11+ (Java 17 recommended)
- Spring Boot 2.5+ or 3.x
- Familiarity with Project Reactor (
Flux,Mono) - Basic understanding of reactive programming concepts
1️⃣ How Backpressure Works in Reactor
At the core of Project Reactor is the Reactive Streams specification, which defines how publishers and subscribers communicate:
- Publisher emits data
- Subscriber consumes data
- Subscriber requests a specific number of items
- Publisher honors that request and does not emit more than requested
This request signaling is what backpressure is all about.
Example:
Flux<Integer> numbers = Flux.range(1, 1_000_000);
numbers.subscribe(new BaseSubscriber<>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
// Request 10 items initially
request(10);
}
@Override
protected void hookOnNext(Integer value) {
System.out.println("Received: " + value);
// Request the next item one by one
request(1);
}
});
This subscriber explicitly controls how many elements it wants.
2️⃣ Typical Backpressure Pitfalls
Problem #1: Consuming a large Flux without limiting request size:
@GetMapping(value = "/numbers", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Integer> streamNumbers() {
return Flux.range(1, 1_000_000);
}
This will push 1 million items as fast as possible—if the client is slow, you will quickly fill buffers.
3️⃣ Applying Rate Limiting with limitRate
One of the simplest ways to manage backpressure is using limitRate():
return Flux.range(1, 1_000_000)
.limitRate(10);
This splits the data flow into chunks—by default, it requests 75% of the limit before re-requesting more.
How it works:
- First requests 10 elements.
- Once 7 are processed (75%), requests 7 more.
- Keeps this window moving.
4️⃣ Buffering and Dropping Strategies
Sometimes you need to buffer or drop excess items:
Buffering
return fastProducer
.onBackpressureBuffer(1000); // Buffer up to 1000 elements
Warning: If your consumer is too slow, buffers will eventually fill.
Dropping
return fastProducer
.onBackpressureDrop(item -> {
log.warn("Dropping item {}", item);
});
This approach drops data that can’t be processed—suitable for telemetry or logs where occasional loss is acceptable.
5️⃣ Applying Backpressure in Streaming APIs
Example Controller
Let’s build an endpoint emitting timestamps every 10ms, which risks overwhelming clients:
@GetMapping(value = "/timestamps", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamTimestamps() {
return Flux.interval(Duration.ofMillis(10))
.map(tick -> "Time: " + Instant.now())
.onBackpressureDrop(t -> log.warn("Dropped tick {}", t))
.limitRate(100);
};
What happens here:
Flux.intervalis fast (emits every 10ms)onBackpressureDropdiscards events if the client can’t keep uplimitRatefurther controls demand
6️⃣ Client-Side Flow Control
If your client is another WebFlux app or a WebClient, it can control how fast it consumes:
WebClient client = WebClient.create();
Flux<String> stream = client.get()
.uri("http://localhost:8080/timestamps")
.retrieve()
.bodyToFlux(String.class)
.limitRate(50); // Limit processing rate
stream.subscribe(System.out::println);
Even if the server produces faster, the client only processes 50 items at a time.
7️⃣ Practical Patterns for Resilience
✅ Use limitRate on producers to chunk delivery
✅ Apply onBackpressureBuffer cautiously, with a buffer size
✅ Use onBackpressureDrop for non-critical data
✅ Avoid blocking operations inside your subscribers
✅ Monitor logs for dropped items or buffer overflows
Example Use Cases
- Stock price streaming: Limit rate to avoid UI lock-ups
- Telemetry pipelines: Drop excess metrics if clients are too slow
- Chat applications: Buffer a limited backlog of messages
Sources & Further Reading
Conclusion
Backpressure is essential for stable, responsive streaming APIs. By understanding and applying Reactor’s built-in operators like limitRate, onBackpressureBuffer, and onBackpressureDrop, you can build systems that gracefully adapt to varying loads and client capabilities.




