Backpressure Patterns — Flow Control for Resilient Distributed Systems
What is backpressure?#
Backpressure is a flow-control mechanism where a slow consumer signals a fast producer to slow down. Without it, the producer floods the consumer, buffers grow unbounded, memory runs out, and the system crashes.
The term comes from fluid dynamics: pressure that opposes the desired flow. In software, it means the same thing — downstream resistance that propagates upstream.
Why backpressure matters#
Consider a data pipeline: an API ingests 10,000 events per second, a message queue buffers them, and a database consumer writes at 2,000 per second. Without backpressure:
- The queue grows by 8,000 messages per second
- After 10 minutes, 4.8 million messages are buffered
- The broker runs out of memory or disk
- The entire pipeline fails
Backpressure prevents this by making the producer aware of the consumer's capacity.
Push-based vs pull-based models#
Push-based (no backpressure)#
The producer sends data whenever it is ready. The consumer must handle whatever arrives:
- TCP without flow control
- Fire-and-forget message publishing
- Event emitters with no subscriber feedback
If the consumer is slower than the producer, data is lost or buffers overflow.
Pull-based (natural backpressure)#
The consumer requests data when it is ready. The producer only sends what was requested:
- HTTP request/response
- Kafka consumer polling
- Iterator/generator patterns
- Reactive Streams
request(n)
The consumer controls the flow rate. Backpressure is built into the protocol.
Hybrid: push with backpressure signals#
The producer pushes data but respects signals from the consumer:
- TCP flow control (window size)
- Reactive Streams with
request(n)demand signaling - gRPC flow control
- WebSocket with drain events
This combines the low-latency of push with the safety of pull.
The Reactive Streams specification#
The Reactive Streams spec (adopted into Java 9 as java.util.concurrent.Flow) defines four interfaces:
- Publisher: produces elements
- Subscriber: consumes elements
- Subscription: the link between them, with
request(n)andcancel() - Processor: both a subscriber and a publisher (a transformation stage)
The protocol:
- Subscriber calls
subscribe()on Publisher - Publisher calls
onSubscribe(subscription)on Subscriber - Subscriber calls
subscription.request(n)to demandnelements - Publisher calls
onNext()up tontimes - Subscriber requests more when ready
No element is sent without demand. This is non-blocking backpressure.
Queue-based buffering strategies#
When producers and consumers run at different speeds, queues absorb bursts. But queues need bounds:
Bounded queues#
Set a maximum size. When full, choose a strategy:
| Strategy | Behavior | When to use |
|---|---|---|
| Block producer | Producer waits until space is available | Batch processing, can tolerate latency |
| Drop newest | Discard incoming messages | Telemetry, metrics (latest is less valuable than baseline) |
| Drop oldest | Discard the oldest buffered message | Real-time displays (stale data is useless) |
| Error | Reject with an error response | APIs where the caller can retry |
Unbounded queues (avoid these)#
Unbounded queues defer the problem. They grow until the system runs out of memory. Always set bounds, even if they are generous.
Priority queues#
When shedding load, shed low-priority work first. Priority queues let critical requests (health checks, payments) proceed while bulk operations (analytics, reports) are deferred.
Load shedding#
When backpressure is not enough — when the system is at capacity and cannot slow producers — load shedding deliberately drops work:
- Random early detection (RED): as queue utilization rises, randomly drop an increasing percentage of incoming requests
- CoDel (Controlled Delay): drop packets that have been in the queue longer than a target latency
- Priority-based shedding: drop low-priority requests first
- Admission control: reject requests at the entry point before they consume internal resources
HTTP load shedding#
Return 503 Service Unavailable with a Retry-After header. Well-behaved clients back off. This is more graceful than letting the server crash under load.
Kafka consumer lag#
If a Kafka consumer falls behind:
- Scale out consumers (add partitions and consumer instances)
- Skip to the latest offset and backfill from a snapshot
- Route lagging partitions to a catch-up consumer group
Circuit breaker integration#
Backpressure and circuit breakers complement each other:
- Backpressure slows the flow when a downstream service is slow
- Circuit breaker stops the flow when a downstream service is failing
Together:
- Consumer slows down (backpressure)
- Latency increases beyond threshold (circuit breaker opens)
- Requests are immediately rejected or routed to a fallback
- After a timeout, the circuit breaker half-opens and tests with a few requests
- If the downstream service recovers, normal flow resumes
This prevents cascading failures: without the circuit breaker, backpressure alone would cause upstream services to accumulate blocked threads and eventually fail too.
Tools and frameworks#
RxJava#
RxJava 2+ supports backpressure through the Flowable type (as opposed to Observable, which does not support backpressure):
onBackpressureBuffer()— buffer with optional boundsonBackpressureDrop()— discard items when downstream is slowonBackpressureLatest()— keep only the most recent item
Project Reactor#
The reactive library for Spring WebFlux. Flux supports backpressure natively:
limitRate(n)— prefetchnelements at a timeonBackpressureBuffer(maxSize, dropStrategy)onBackpressureDrop()- Integrates with Spring WebFlux, R2DBC, and reactive MongoDB drivers
Akka Streams#
Akka Streams implements Reactive Streams with a graph-based DSL:
- Every stage has automatic backpressure
buffer(size, OverflowStrategy)— explicit buffering with overflow controlthrottle(elements, duration)— rate limiting- Fusing: adjacent stages run on the same thread to reduce overhead
- Async boundaries: stages on different threads communicate via backpressured message passing
Kafka#
Kafka provides natural backpressure through pull-based consumption:
- Consumers poll at their own pace (
max.poll.recordscontrols batch size) - Producers can be throttled by broker quotas
- Consumer lag metrics signal when consumers fall behind
gRPC#
gRPC has built-in flow control at the HTTP/2 layer:
- Per-stream and per-connection flow control windows
- Servers can delay responses to signal backpressure
- Client-side streaming respects server readiness
Backpressure anti-patterns#
- Unbounded queues everywhere — defers failure, does not prevent it
- Ignoring consumer lag — if you do not monitor it, you do not know you have a problem
- Retrying without backoff — amplifies load on an already-stressed system
- Blocking the event loop — in async systems, blocking defeats the purpose of non-blocking backpressure
- Shedding load too late — by the time the system is overloaded, shedding may not recover it
Monitoring backpressure#
Key metrics to watch:
- Queue depth: how many messages are buffered
- Consumer lag: how far behind the consumer is (Kafka, event streams)
- Processing latency: time from enqueue to dequeue
- Rejection rate: how many requests are being shed
- Thread pool saturation: how many threads are blocked waiting
Alert when queue depth or consumer lag grows steadily — it means the consumer cannot keep up.
Visualize backpressure in your architecture#
On Codelit, generate a streaming data pipeline or microservices architecture to see where backpressure applies. Click on queues and service boundaries to explore buffering strategies and flow control.
This is article #230 in the Codelit engineering blog series.
Design resilient architectures visually at codelit.io.
Explore the WhatsApp architecture interactively
Try it →Try these templates
Continue learning
Go deeper on system design
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
Building Microservices
Sam Newman · 2021
600 pages on splitting a monolith: service boundaries, data decomposition, migration order.
Designing Data-Intensive Applications
Martin Kleppmann, Chris Riccomini · 2026
Traces replication, consistency and consensus end to end, with the failure modes behind each choice.
3.6 (196)#1 in Data MiningDatabase Internals
Alex Petrov · 2019
Explains B-trees, LSM trees and consensus protocols so you can predict how a datastore will behave.
4.6 (560)#2 in Management Information SystemsRelease It!
Michael T. Nygard · 2018
Post-mortems of real outages, then the stability patterns (circuit breaker, bulkhead) that stop them.
4.6 (501)#4 in Client-Server Networking Systems