Mastering Backpressure in Spring WebFlux: Strategies for Slow Clients
This article explains how backpressure works in Spring WebFlux, outlines the built‑in Reactor strategies, provides practical code examples for handling slow consumers, and offers guidance on choosing and monitoring the right approach to keep reactive applications resilient under load.
Using Spring WebFlux for reactive programming is a powerful way to build highly scalable applications, but a key concern is backpressure. When the publisher (server) emits data faster than the subscriber (client) can consume it, resources can be exhausted, memory leaks may occur, and the application can crash. This article explores how to implement backpressure in Spring WebFlux to gracefully handle slow clients.
Understanding the problem: slow consumers and resource exhaustion
Imagine a Spring WebFlux endpoint streaming data to many clients. Some clients have slow network connections, limited processing power, or temporary delays. If the server continues pushing data at full speed, these slow clients fall behind, causing a backlog that can lead to:
Memory overflow errors : the server may try to buffer all data for the slow client, eventually exceeding available memory.
Increased latency : all clients (including fast ones) may experience higher latency as the server spends time managing the backlog.
System instability : in extreme cases the server can become unresponsive and crash due to resource exhaustion.
Backpressure is a mechanism that allows the subscriber (client) to signal the publisher (server) that it cannot keep up and requests data at a manageable rate.
WebFlux and Reactor backpressure support
Spring WebFlux leverages the Reactor library, which provides strong backpressure support through the reactive‑streams specification. The Flux and Mono types are inherently backpressure‑aware. Reactor offers several backpressure strategies that define how a publisher should react when a subscriber cannot keep up.
Reactor backpressure strategies
Common Reactor backpressure strategies include:
BUFFER : (default for some operators) buffers all emitted items until the subscriber is ready. Use with caution! In unbounded streams this can cause unlimited buffering and memory overflow.
DROP : discards the most recent items when the subscriber is not ready, sacrificing data to maintain throughput.
LATEST : keeps only the latest emitted item, discarding older ones—useful for scenarios where only the newest data matters (e.g., real‑time updates).
ERROR : signals an IllegalStateException to the subscriber when it cannot keep up, useful for immediate detection of backpressure problems.
IGNORE : silently drops all items when the subscriber is not ready; generally discouraged because it provides no feedback.
onRequest() (manual backpressure) : allows the subscriber to explicitly request a specific number of items via request(), giving fine‑grained control at the cost of more implementation effort.
Implementing backpressure in Spring WebFlux: code example
Below is a simple controller that streams data using Server‑Sent Events (SSE) and demonstrates each backpressure strategy.
1. Project setup: Create a new Spring Boot project with the spring-boot-starter-webflux dependency.
2. Data source: Simulate a data generator that emits a data point at a fixed interval.
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.time.LocalTime;
public class DataGenerator {
public static Flux<String> generateData() {
return Flux.interval(Duration.ofMillis(100))
.map(i -> "Data Point: " + LocalTime.now());
}
}3. Controller with backpressure strategies:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api")
public class BackpressureController {
private final DataGenerator dataGenerator = new DataGenerator();
@GetMapping(value = "/buffer", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> buffer() {
return dataGenerator.generateData().onBackpressureBuffer(); // explicit buffer, default settings
}
@GetMapping(value = "/drop", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> drop() {
return dataGenerator.generateData().onBackpressureDrop();
}
@GetMapping(value = "/latest", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> latest() {
return dataGenerator.generateData().onBackpressureLatest();
}
@GetMapping(value = "/error", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> error() {
return dataGenerator.generateData().onBackpressureError();
}
@GetMapping(value = "/manual", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> manual() {
return dataGenerator.generateData()
.onBackpressureBuffer(10) // buffer size 10, excess will be dropped
.limitRate(5); // subscriber requests 5 items at a time (throttling)
}
@GetMapping(value = "/withTimeout", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> withTimeout() {
return dataGenerator.generateData()
.timeout(Duration.ofSeconds(1)) // timeout if subscriber does not request within 1 s
.onErrorResume(e -> Flux.just("Timeout Occurred: " + e.getMessage()));
}
}Explanation:
@GetMapping(value = "/...", produces = MediaType.TEXT_EVENT_STREAM_VALUE)tells Spring to return an SSE stream. dataGenerator.generateData() provides a Flux of data points.
The various .onBackpressure…() operators apply the selected backpressure strategy to the Flux.
The /manual endpoint demonstrates manual backpressure using onBackpressureBuffer(10) and limitRate(5), which limits the subscriber to 5 items per request.
The /withTimeout endpoint shows how timeout() can detect slow clients and handle the TimeoutException with onErrorResume() (or onErrorReturn()).
Testing the endpoints
You can test the endpoints with curl or a browser, e.g.: curl -N http://localhost:8080/api/drop The -N flag disables buffering in curl so you can observe the streaming behavior. Try slowing your network or pausing output to simulate a slow client.
Simulating a slow client
To truly test backpressure, simulate a slow client. Options include network throttling (e.g., using tc on Linux) or adding artificial delay in client code. Example JavaScript that adds a 500 ms delay after each event:
const eventSource = new EventSource('http://localhost:8080/api/drop');
eventSource.onmessage = (event) => {
setTimeout(() => {
console.log('Received:', event.data);
}, 500); // simulate 500 ms delay
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
};Choosing the right backpressure strategy
Guidelines for selecting a strategy: DROP or LATEST: suitable when occasional data loss is acceptable and only the newest information matters (e.g., stock ticker, sensor data). ERROR: useful for debugging and quickly identifying backpressure issues.
Manual backpressure ( onRequest()): provides the most control but requires additional implementation; use when you need fine‑grained flow control. timeout(): apply when a subscriber must request data within an acceptable time window.
Important considerations
Monitor your application : use metrics (Prometheus, Grafana) to track dropped items, endpoint latency, and resource usage.
Client implementation : ensure clients are configured to handle backpressure, possibly with retries or local buffering.
Buffer size : when using BUFFER or onBackpressureBuffer, start with a small buffer and increase gradually while monitoring resource consumption.
Beyond basic backpressure
Study the Reactive Streams specification to fully leverage its capabilities.
Create custom Reactor operators for domain‑specific backpressure needs.
Implement circuit breakers to prevent cascading failures when downstream clients become slow or unavailable.
Apply rate limiting at the API gateway or service level to protect the server from being overwhelmed.
Conclusion
Backpressure is a critical consideration when building reactive applications with Spring WebFlux. By understanding the available strategies and implementing them correctly, you can create resilient, scalable systems that gracefully handle slow clients and avoid resource exhaustion. Continuously monitor and tune your backpressure configuration to ensure smooth performance under high load.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
