Filter concepts by levelShowing all levels.

System Design · Backpressure

Backpressure

Why a fast producer needs a way to feel a slow consumer, the five mechanisms that provide it, and the signals that actually catch it failing

  • The mismatchproducer rate vs. consumer drain rate
  • Bounded queuethe hard ceiling every other mechanism relies on
  • Flow controlconsumer pulls only what it can handle
  • Age over lengtholdest-item wait time beats raw queue count

1.What backpressure is and why it matters

Backpressure is any mechanism that stops a fast producer from writing work faster than a consumer can process it. Without one, the gap between the two rates has to live somewhere — usually an in-memory queue that grows until the process runs out of memory and crashes.

Key features

  • A producer and a consumer rarely process at the same rate — a spike in traffic, a slow database write, or a consumer restart all widen the gap between write rate and drain rate
  • Without backpressure, the gap is absorbed by an unbounded queue or buffer — memory usage grows linearly with how far behind the consumer falls, with no ceiling
  • The failure mode is not gradual: memory grows quietly until the process hits its limit, then the process is killed (out-of-memory) or starts swapping, and every message still in the buffer is at risk of being lost
  • Backpressure is a property of the whole pipeline, not a single component — a fix at the queue does nothing if the producer itself has no way to learn it should slow down
  • The Reactive Streams specification (adopted into Java's Flow API and reactive libraries) treats backpressure as a first-class contract: a consumer explicitly signals how much it can accept, rather than a producer pushing blindly

The producer/consumer mismatch and its consequence without backpressure

The producer/consumer mismatch and its consequence without backpressure
ScenarioProducer rate vs consumer rateWhat happens with no backpressure
Traffic spikeProducer rate jumps well above consumer's steady-state drain rateQueue depth grows for the duration of the spike, unbounded
Slow downstream dependencyConsumer rate drops (e.g. a database write now takes 5x longer)Queue depth grows even though producer rate never changed
Consumer crash or restartConsumer rate drops to zero for a periodQueue depth grows at the full producer rate until the consumer returns
Same traffic spike, with and without backpressure

No backpressure

  • Producer writes at full rate no matter what
  • Queue grows without a ceiling during the spike
  • Memory usage climbs until the process is killed
  • Messages already buffered are lost on crash

With backpressure

  • Producer is signaled to slow down or blocked
  • Queue depth stays within a known bound
  • Memory usage stays flat and predictable
  • Producer, not the queue, absorbs the slowdown
  • No backpressure
    • Producer writes at full rate no matter what
    • Queue grows without a ceiling during the spike
    • Memory usage climbs until the process is killed
    • Messages already buffered are lost on crash
  • With backpressure
    • Producer is signaled to slow down or blocked
    • Queue depth stays within a known bound
    • Memory usage stays flat and predictable
    • Producer, not the queue, absorbs the slowdown

Command

text
# Unbounded: producer never checks consumer capacity
while True:
    item = generate_work()
    queue.put(item)          # always succeeds, queue grows forever

# With backpressure: producer is forced to slow down
while True:
    item = generate_work()
    queue.put(item, block=True, timeout=None)  # blocks once queue is full

Backpressure exists because a producer and consumer rarely process at the same rate, and the gap has to go somewhere. Without a bound on the queue, "somewhere" is process memory, and the failure looks like a sudden crash after a period of no visible warning.

2.Backpressure mechanisms: bounded queues, rate limits, flow control, batch size, consumer scaling

Five mechanisms apply backpressure in practice: a bounded queue gives the mismatch a hard ceiling, a rate limit caps the producer directly, flow control lets the consumer pull only as much as it can handle, batch sizing controls how much work one unit of processing represents, and consumer scaling adds drain capacity. Real systems combine several rather than relying on just one.

Key features

  • A bounded queue rejects, blocks, or drops new work once full — the exact policy (reject with an error, block the producer, or drop the oldest/newest item) is a deliberate choice, not a default
  • A rate limit caps the producer's write rate directly, independent of current queue depth — it prevents the mismatch from opening in the first place rather than reacting after work has already queued
  • Flow control (the model behind TCP's sliding window and the Reactive Streams `request(n)` method) inverts control: the consumer explicitly tells the producer how many items it can accept next, so the producer never sends more than the consumer asked for
  • Batch size controls the unit of work moving through the pipeline — larger batches amortize per-message overhead but increase the latency and memory cost of any single unit, and a batch that is too large can itself overwhelm a consumer in one shot
  • Consumer scaling (adding more workers or partitions) raises the drain rate, but only helps if the workload is parallelizable — a single ordered stream with one logical consumer gets no benefit from adding workers that must still process in order

The five mechanisms and where each one applies pressure

The five mechanisms and where each one applies pressure
MechanismWhere it actsTrade-off
Bounded queueCaps how much unprocessed work can accumulateFull queue forces an explicit choice: reject, block, or drop
Rate limitCaps the producer's write rate directlySimple and predictable, but a fixed cap can throttle legitimate spikes
Flow controlConsumer pulls only what it can handle (demand-based)More precise than a fixed rate limit, but needs a protocol both sides support
Batch sizeControls how much work one processing unit representsBigger batches cut overhead but raise per-batch latency and memory
Consumer scalingRaises total drain rate by adding workers or partitionsOnly helps if the work can be safely parallelized
Where each mechanism sits in the pipeline
cappedwrite ratebatcheditemspulled ondemand

Producer

rate limit applied here

Bounded queue

batch size groups items here

Flow control

consumer signals demand

Consumers

scaled out for drain rate

  • Producer — rate limit applied here
    • leads to Bounded queue (capped write rate)
  • Bounded queue — batch size groups items here
    • leads to Flow control (batched items)
  • Flow control — consumer signals demand
    • leads to Consumers (pulled on demand)
  • Consumers — scaled out for drain rate

Command

text
# Flow control: consumer requests exactly what it can handle
subscriber.request(100)          # "send me up to 100 items"
producer.onRequest(n -> {
    send(next(n));                # never sends more than requested
});

# Rate limit: producer capped independent of queue state
if not rate_limiter.allow():
    reject_or_delay(item)
else:
    queue.put(item)

A bounded queue, a rate limit, flow control, batch sizing, and consumer scaling each apply pressure at a different point in the pipeline. Real systems combine several — a bounded queue as the hard ceiling, plus a rate limit or flow control to keep the queue from filling in the first place, plus scaling to raise drain rate over time.

3.Monitoring backpressure: queue age and lag, not only queue length

Queue length alone hides whether a backlog is actually a problem — the more honest signals are queue age (how long the oldest unprocessed item has been waiting) and consumer lag (how far behind the consumer is, in messages or time). A short queue can still mean an unacceptable wait if the consumer is stuck.

Key features

  • Queue length (raw count of items waiting) says nothing about time — 10,000 items draining at 5,000/sec is a non-event, 10,000 items draining at 5/sec is a multi-hour backlog, and length alone cannot tell the two apart
  • Queue age (how long the single oldest unprocessed item has been waiting) directly measures the thing users and SLAs actually care about: how stale is the oldest thing still waiting
  • Consumer lag (how many messages, or how much time, the consumer is behind the producer's latest write) is the standard term in log-based systems — Kafka reports lag per partition as the offset gap between the latest produced message and the last one a consumer group has committed
  • A consumer that is alive but stuck (deadlocked, crash-looping, or repeatedly failing and retrying the same message) can show a perfectly stable or even shrinking queue length while age and lag climb without bound
  • Alerting on length alone produces both false negatives (a genuinely stuck consumer with a small backlog looks fine) and false positives (a large but fast-draining queue looks alarming when it is not)

The three signals and what each one actually tells you

The three signals and what each one actually tells you
SignalWhat it measuresWhat it misses
Queue lengthRaw count of unprocessed items right nowSays nothing about drain rate or how long items have waited
Queue ageHow long the oldest unprocessed item has been waitingNeeds a per-item timestamp; not always tracked by default
Consumer lagHow far behind the consumer is vs. the producer (messages or time)Requires the broker/queue to expose committed offset vs. latest offset
Same queue length, opposite reality

Queue length: 500 (both cases)

  • Case A: draining at 1,000/sec — clears in 0.5s
  • Case B: consumer stuck, draining at 0/sec — growing stale
  • Length alone reports "500" for both, no distinction

Oldest-item age tells them apart

  • Case A: oldest item is 0.4s old — healthy
  • Case B: oldest item is 20 minutes old — stuck consumer
  • Age (or lag) exposes the real state length hides
  • Queue length: 500 (both cases)
    • Case A: draining at 1,000/sec — clears in 0.5s
    • Case B: consumer stuck, draining at 0/sec — growing stale
    • Length alone reports "500" for both, no distinction
  • Oldest-item age tells them apart
    • Case A: oldest item is 0.4s old — healthy
    • Case B: oldest item is 20 minutes old — stuck consumer
    • Age (or lag) exposes the real state length hides

Command

text
# Queue length -- cheap, but incomplete
metric('queue.length', queue.size())

# Queue age -- the oldest item's wait time
oldest = queue.peek_oldest()
metric('queue.oldest_age_seconds', now() - oldest.enqueued_at)

# Consumer lag (Kafka-style) -- offset gap per partition
lag = latest_offset(partition) - committed_offset(consumer_group, partition)
metric('consumer.lag', lag)

Queue length tells you volume, not health. Queue age (oldest item's wait time) and consumer lag (offset or time behind the producer) tell you whether the consumer is actually keeping up — monitor those, and alert on them, not just the raw count.

Remember

  • A producer and consumer rarely process at the same rate — without a bound, the gap becomes unbounded memory growth and an eventual crash.
  • Bounded queues, rate limits, flow control, batch sizing and consumer scaling each apply pressure at a different point — real systems combine several.
  • Consumer scaling has a reaction lag (provisioning takes seconds to minutes); a bounded queue or rate limit acts immediately and should not be replaced by scaling alone.
  • Queue length tells you volume, not health — queue age and consumer lag tell you whether the consumer is actually keeping up.

Pro tip

  • Pick an explicit full-queue policy (reject, block, or drop-oldest) — an unstated default is usually "block forever," which just moves the mismatch upstream.
  • Alert on the oldest unprocessed item's age or per-partition consumer lag, not on raw queue length — a stuck consumer with a small backlog looks fine on length alone.

Next up

  • File and large data processing
  • Distributed counters
  • Idempotency keys

Sources

  • Reactive Streams specification — reactive-streams.org
  • RabbitMQ flow control docs — rabbitmq.com/docs
  • Kafka consumer lag docs — kafka.apache.org/documentation
  • Checked 2026-08-25

Spotted a mistake on this page? Tell us.