Skip to content

Backpressure

Why Does Backpressure Exist?

A producer generating work faster than a consumer can process it — that mismatch has to go somewhere. Without a way to signal "slow down," it goes into an ever-growing queue or a memory footprint that keeps climbing until something breaks: the system runs out of resources, or latency stretches out as the backlog grows. Backpressure is the fix — a slower, downstream component telling the faster, upstream one to back off, buffer with a limit, or shed load, instead of the mismatch quietly piling up until it can't anymore.

Producer generates work FASTER than the consumer can process it

↓

WITHOUT backpressure:  work piles up unboundedly (a growing queue,
                        growing memory usage) until the system runs
                        out of resources and fails

↓

WITH backpressure:      the consumer (or something in between)
                        signals back to the producer to slow down,
                        buffer with a known limit, or explicitly
                        shed excess work

The Same Idea, Recurring at Every Layer of the Stack

Backpressure isn't one technique — it's a pattern that shows up, in different clothes, at nearly every layer already covered in this handbook. Spotting it across these is the real point of this chapter.

TCP Flow Control (from the TCP chapter):

  The RECEIVER advertises how much buffer space it has, and the
  sender won't send more than that — the receiver signaling
  "don't overwhelm me" directly to the sender.

TCP Congestion Control (from the TCP chapter):

  The NETWORK itself signals congestion (via packet loss), and
  the sender backs off — the same principle, applied against
  the network's own capacity rather than just the receiver's.

Kafka's Pull-Based Consumption (from the Kafka chapter):

  Consumers PULL messages at their own pace, rather than having
  messages pushed at them unconditionally — the consumer itself
  controls how much work it takes on at once, rather than being
  forced to accept work faster than it can handle.

Circuit Breakers (from the Circuit Breaker chapter):

  Failing FAST once a downstream dependency is known to be
  struggling, instead of continuing to send it more requests it
  can't handle — a form of backpressure applied specifically
  during a confirmed failure, protecting BOTH sides.

Rate Limiting (from the Rate Limiting chapter):

  Explicitly REJECTING excess requests beyond a configured
  threshold, rather than accepting unlimited load and letting the
  system degrade or crash trying to handle all of it.

Same underlying move every time — a constrained component makes its limit explicit instead of silently soaking up unlimited demand. Seeing that thread across four unrelated-looking topics is worth more than memorizing each one separately.


Strategies When You Control Both Ends

If you're designing this yourself, rather than leaning on TCP's or Kafka's built-in handling, there are a few real options.

Blocking

Producer tries to add more work → consumer's buffer is full →
producer BLOCKS (waits) until space frees up

Simple, and nothing gets lost — but it can cascade. If something's calling the now-blocked producer, that caller starts waiting too, and the slowdown can travel further up the chain than you intended.

Dropping

Producer tries to add more work → consumer's buffer is full →
the EXCESS work is discarded rather than queued

Right when losing some data beats unbounded memory growth or cascading blocks — some telemetry and metrics pipelines take this trade deliberately, since a slightly incomplete picture beats an outage from unbounded buffering.

Bounded Buffering

Allow SOME slack/buffering — but with an EXPLICIT CAP

Queue capacity: 10,000 items MAXIMUM

↓

If the queue is full, apply blocking OR dropping (a deliberate
choice) — but memory usage has a KNOWN, bounded ceiling either way,
rather than growing without limit

This is the detail that separates a real design from a common anti-pattern: "just add a queue" without a bound doesn't fix a producer/consumer mismatch. It delays the crash while quietly eating memory, and usually makes the eventual failure harder to diagnose, since by then the queue has grown enormous.


Why an Unbounded Queue Is a Dangerous Anti-Pattern

"We added a queue between the producer and consumer, problem solved"

↓

If the queue has NO explicit size limit:

↓

Producer keeps adding faster than consumer processes

↓

Queue grows... and grows... and grows...

↓

Eventually: the process runs out of memory and crashes anyway —
             just LATER, and with a much larger, harder-to-diagnose
             backlog than if the mismatch had been addressed
             explicitly from the start

An unbounded queue doesn't solve the mismatch, it just postpones the reckoning — trading an obvious, easy-to-diagnose problem now for a bigger, uglier crash later once memory finally runs out. A bounded queue with an explicit policy for what happens when it fills — block, drop, reject — is what actually deals with the mismatch instead of hiding it.


Real-World Examples

Reactive Streams (RxJava and Similar Frameworks)

A formal spec that makes backpressure a first-class part of the producer/consumer contract — the subscriber signals how much it can take, and the publisher is required to respect it instead of pushing blindly.


Kubernetes Resource Limits

Backpressure at the infrastructure level. A pod trying to use more memory than its limit gets OOM-killed, rather than being allowed to consume without bound and take the whole node down with it. The limit is the signal, enforced by the platform.


TCP Flow and Congestion Control

Covered in full in the TCP chapter — the textbook, foundational case of backpressure, running underneath essentially every higher-level protocol in this handbook.


Common Interview Questions

What is backpressure, and can you name examples of it at different layers of the stack?

A downstream or capacity-constrained component signaling back to a faster upstream one to prevent unbounded pile-up. TCP's flow control and congestion control at the transport layer, Kafka's pull-based consumption letting consumers set their own pace, circuit breakers failing fast instead of hammering a struggling dependency, and rate limiting rejecting excess requests outright.


Why is an unbounded queue a dangerous anti-pattern, even though it seems to "solve" a momentary producer/consumer speed mismatch?

Because it doesn't fix the mismatch, it just defers it. The queue keeps growing as long as the producer outpaces the consumer, and eventually exhausts memory anyway — later, and with a bigger, harder-to-diagnose failure than if the mismatch had been handled explicitly from the start.


Blocking vs dropping vs bounded buffering as backpressure strategies — what are the trade-offs?

Blocking loses nothing but can cascade the slowdown to whatever calls the producer. Dropping loses some data but avoids unbounded growth and cascading blocks. Bounded buffering allows some slack with a known ceiling, paired with a deliberate block-or-drop policy once that ceiling is hit.


How does Kafka's pull-based consumption model relate to backpressure?

Consumers request the next batch only when they're ready for more, instead of the broker pushing at whatever rate messages arrive — the consumer controls its own pace, which is backpressure built directly into the consumption model.


How does a circuit breaker relate to backpressure?

By failing fast once a dependency is confirmed struggling, it stops sending it requests it can't handle — protecting both the struggling service and the caller's own resources. It's backpressure specifically for a confirmed, sustained failure.


Common Mistakes

❌ Adding an unbounded queue as a "fix" for a producer/consumer speed mismatch.

Delays and obscures the eventual crash instead of preventing it — memory keeps growing until it's gone, and the failure that follows is usually worse than if the mismatch had been handled up front.


❌ Treating TCP flow control, Kafka's pull model, circuit breakers, and rate limiting as unrelated, isolated techniques.

Missing the pattern connecting all of them is missing the point — recognizing backpressure as one idea applied differently at each layer is what real depth looks like here.


❌ Choosing blocking as a backpressure strategy without considering its cascading effect upstream.

A blocked producer can become a bottleneck for whatever calls it, propagating the slowdown further up the chain than intended — a close cousin of the retry amplification problem from the Distributed Systems section.


Key Takeaways

  • Backpressure is a downstream or capacity-constrained component signaling back to a faster upstream one, preventing unbounded work from silently piling up until something fails.
  • The same idea recurs throughout this handbook — TCP's flow and congestion control, Kafka's pull model, circuit breakers failing fast, rate limiting's rejection of excess load. Spotting the connection is real synthesis, not rote memorization.
  • Blocking, dropping, and bounded buffering are the three real strategies when you're designing this yourself, each trading off guaranteed delivery, data loss, and bounded resource use differently.
  • An unbounded queue is a common, dangerous anti-pattern — it delays and worsens the eventual failure instead of preventing it.
  • Backpressure only works when it's explicit and bounded, with a clear policy for what happens at the limit — otherwise you're just hiding the mismatch, not solving it.