Overview
Backpressure is the mechanism by which consumers signal producers to slow down. Without it, a fast producer overwhelms a slow consumer — buffers fill, memory exhausts, or items are silently dropped. In Project Reactor, backpressure flows via the request(n) signal from Subscriber to Publisher. In practice, most pipelines don't manually manage backpressure — but understanding what happens when you violate it (system crashes, dropped events, OOM) is essential for production reactive systems.
Architecture
BACKPRESSURE IN A REQUEST PIPELINE
════════════════════════════════════════════════════════
HTTP Client → WebFlux Handler → DB (R2DBC) → WebFlux Response
│ │ │ │
sends data requests 10 items fetches 10 rows writes 10 items
at 1000 req/s at a time at a time at 10/s
│ │ │ │
│ ← request(10) ──────────────────────────┘
│ │
HTTP buffers requests(10) to DB
(TCP receive processes 10
window fills) ← request(10) when done
End-to-end: TCP flow control → Reactor request(n) → DB cursor fetch size
OVERFLOW STRATEGIES (when producer ignores backpressure)
════════════════════════════════════════════════════════
onBackpressureBuffer(n) → buffer up to n items in memory, error on overflow
onBackpressureBuffer(∞) → unbounded buffer (OOM risk!)
onBackpressureDrop(fn) → discard newest items when buffer full
onBackpressureLatest() → keep only latest item, drop intermediate
onBackpressureError() → emit OverflowException immediately (fast-fail)
USE CASE MAPPING:
Metrics / telemetry: onBackpressureDrop (ok to lose some data points)
Event log: onBackpressureBuffer(bounded) (must not lose)
Real-time price feed: onBackpressureLatest (stale prices useless)
Critical payments: onBackpressureError (fail fast, don't buffer money)
HOT SOURCE WITH SLOW SUBSCRIBER
════════════════════════════════════════════════════════
Kafka consumer:
Partition → Reactor Kafka (hot)
├── records arrive at broker rate
├── subscriber processes at business logic rate
└── Flow: reactor-kafka applies backpressure to Kafka polling
(stops calling consumer.poll() when downstream can't keep up)
(Kafka offset not committed until item processed)
Technical Implementation
Overflow Strategies for Different Use Cases
@Service
public class MetricsProcessor {
// Telemetry: ok to drop data under load — system self-heals
public Flux<Metric> processMetrics(Flux<Metric> source) {
return source
.onBackpressureDrop(dropped ->
log.warn("Dropping metric under backpressure: {}", dropped.getName())
)
.flatMap(
metric -> enrichMetric(metric),
16 // max 16 concurrent enrichments
);
}
// Audit log: must not lose events — buffer bounded, fail on overflow
public Flux<AuditEvent> processAuditEvents(Flux<AuditEvent> source) {
return source
.onBackpressureBuffer(
1000, // buffer up to 1000 events
dropped -> {
log.error("Audit event dropped! THIS IS A BUG: {}", dropped);
alertingService.sendAlert("audit-event-dropped");
},
BufferOverflowStrategy.DROP_OLDEST // or ERROR to fail fast
)
.flatMap(event -> auditRepo.save(event));
}
// Real-time dashboard: only care about latest state
public Flux<PriceUpdate> streamLatestPrices(Flux<PriceUpdate> source) {
return source
.onBackpressureLatest() // if slow, only process most recent price
.distinctUntilChanged(PriceUpdate::price) // skip if price unchanged
.doOnNext(update -> webSocketBroadcast(update));
}
}
Controlling Concurrency to Manage Backpressure
// Problem: flatMap with no concurrency limit overwhelms downstream
// Solution: use flatMap's concurrency parameter OR limitRate
public Flux<EnrichedOrder> processOrders(Flux<String> orderIds) {
return orderIds
.limitRate(50) // request 50 items at a time from upstream
.flatMap(
id -> fetchAndEnrich(id),
20, // max 20 concurrent downstream calls
5 // prefetch: pre-request 5 more when 75% done
)
.subscribeOn(Schedulers.boundedElastic());
}
// Reactor Kafka: backpressure from business logic → poll throttle
// Only 100 records in flight at any time
@Bean
public Flux<ReceiverRecord<String, Order>> kafkaOrderStream(ReactiveKafkaConsumerTemplate<String, Order> template) {
return template.receive()
.flatMap(
record -> processRecord(record)
.doOnSuccess(v -> record.receiverOffset().acknowledge()),
100, // max 100 concurrent records being processed
1 // prefetch 1 batch
);
}
Testing Backpressure with StepVerifier
@Test
void shouldHandleBackpressureFromSlowSubscriber() {
// Publisher produces 10 items, subscriber requests only 1 at a time
Flux<Integer> source = Flux.range(1, 10)
.log("source"); // log() shows request(n) signals in output
StepVerifier.create(source, 1) // initial request = 1
.expectNext(1)
.thenRequest(2) // request 2 more
.expectNext(2)
.expectNext(3)
.thenRequest(Long.MAX_VALUE) // request everything remaining
.expectNextCount(7)
.verifyComplete();
}
@Test
void shouldDropWhenOverflowing() {
AtomicInteger droppedCount = new AtomicInteger();
Flux<Integer> source = Flux.range(1, 100)
.onBackpressureDrop(d -> droppedCount.incrementAndGet());
// Subscriber requests only 10 — 90 items should be dropped
StepVerifier.create(source, 10)
.expectNextCount(10)
.thenCancel()
.verify();
assertThat(droppedCount.get()).isGreaterThan(0);
}
Interview Preparation
Q: What happens if you subscribe to a hot Flux without any backpressure handling?
The hot publisher emits regardless of how fast the subscriber processes. If the subscriber is slower, Project Reactor's default behavior is to throw reactor.core.Exceptions$OverflowException: Could not emit tick N due to lack of requests — this is the "fail fast on overflow" behavior. The subscription terminates with an error. To handle this gracefully, add an explicit overflow strategy before the slow subscriber: onBackpressureBuffer, onBackpressureDrop, or onBackpressureLatest depending on your use case. The choice reflects your data contract: can you lose data? Do you need the latest? Must you preserve all?
Q: How does backpressure work end-to-end from an HTTP client through to the database?
At the HTTP layer, Spring WebFlux/Netty reads request data into a buffer and triggers request(n) to the upstream handler only when the downstream is ready. The handler uses reactive repositories (R2DBC), which in turn request rows from the database cursor only as fast as the downstream can consume them. The R2DBC driver keeps a fetch size (similar to JDBC setFetchSize) and fetches the next batch from the database only when the reactive chain requests more items. For HTTP responses, Netty's write buffer signals when the client's TCP receive window is full — Spring WebFlux pauses emission until the network can absorb more data. This creates an end-to-end pressure signal from the HTTP client all the way back to the database cursor.
Q: When would you choose onBackpressureDrop over onBackpressureBuffer?
Use onBackpressureDrop when losing some data is acceptable and the data is time-sensitive (new data makes old data obsolete). Examples: metrics/telemetry (a missing data point doesn't break the system), real-time event streams where stale events have no value, or high-frequency sensor data where only recent readings matter. Use onBackpressureBuffer when data loss is unacceptable (audit logs, financial transactions, order events) and you need a cushion to absorb bursts. Size the buffer based on expected burst magnitude vs sustained processing rate. Always monitor buffer size in production — a continuously full buffer means you're consistently producing faster than consuming, which requires scaling.