Systems · Reliability

Push Back.

Connect a fast producer to a slower consumer through a queue and something has to give. The queue is a shock absorber for temporary bursts — but if the producer is persistently faster, the queue fills up, and now you have a choice. Do nothing and the system either drops data or lets the queue grow until it runs out of memory and crashes. Or apply backpressure: when the queue is full, signal the producer to slow down or stop, so it produces no faster than the consumer can drain — trading a bit of producer throughput for a system that stays alive and bounded. Toggle backpressure on and off and watch the same overload stay healthy or blow up.

a queue absorbs bursts, not a permanent rate mismatch · backpressure bounds memory & prevents drops
producer / consumer

One side generates work, the other processes it — often at different rates.

bounded queue

A buffer with a fixed capacity that absorbs short bursts between the two.

overflow

When a full queue keeps receiving: it must drop items or grow unbounded (crash).

backpressure

A full queue signals the producer to slow or stop — bounding the system.

backpressure.js — a full queue pushes back
Ready
A producer makes items faster than the consumer drains them, through a bounded queue (capacity 10). Each tick both run. With backpressure off, a full queue drops items; with it on, the producer slows to match. Tick to run.
0/10
queue depth
0
dropped
off
backpressure

How it works

The key realization is that a queue only smooths temporary imbalance, not a permanent one. If the producer’s average rate exceeds the consumer’s, no queue size is big enough — it just delays the inevitable. When the buffer fills, you’re out of good options unless you change the producer’s behavior: dropping loses data (sometimes acceptable — old sensor readings, best-effort logs — often not), and an unbounded queue trades a crash-later for a crash-now as memory runs out and latency climbs to uselessness (every item now waits behind a huge backlog). Backpressure fixes the root cause by propagating the “I’m full” signal upstream so the producer generates work no faster than it can be consumed — TCP’s flow-control window does this for networks, reactive streams and gRPC do it for application data, and thread-pool queues with bounded capacity do it for services. The cost is honest: the producer must slow down, but the alternative is lost data or a dead system.

1

A queue absorbs bursts

Between a producer and a slower consumer, a bounded queue buffers short spikes so the consumer can catch up during lulls. This works only while the average rates match.

2

A persistent mismatch fills it

If the producer is consistently faster, the queue trends toward full — the buffer delays the problem but cannot solve a permanent rate imbalance.

3

Full queue → drop or blow up

Without backpressure, a full queue must either drop incoming items (data loss) or grow without bound (rising latency, then out-of-memory crash). Neither is a real fix.

Backpressure slows the producer

Propagate the "full" signal upstream so the producer stops or slows to the consumer’s pace. The queue stays bounded, nothing is dropped, and the system stays stable — at the cost of the producer’s speed.

Queue smooths
bursts only
No backpressure
drop or OOM
Backpressure
bounded, stable
Examples
TCP window, reactive streams

The code

# producer with backpressure on a bounded queue def produce(item): if queue.is_full(): if backpressure: block_until_space() # slow producer to consumer's pace else: drop(item); dropped += 1 # or grow unbounded -> OOM return queue.put(item) def consume(): # slower than producer item = queue.get() process(item) # frees a slot -> unblocks producer

Quick check

1. What can a bounded queue actually fix?

2. What happens to a full queue with no backpressure?

3. What does backpressure do?

FAQ

What is backpressure?

A mechanism where a slow or full downstream component signals upstream producers to slow or stop, so the producer can’t overwhelm a bounded buffer. It keeps the system stable and memory-bounded by matching producer rate to consumer rate, instead of dropping data or crashing. It appears in TCP flow control, reactive streams, message queues, and bounded thread pools.

Why can’t you just use a bigger queue?

A bigger queue only absorbs larger bursts; it can’t fix a persistent rate mismatch. Any finite queue fills eventually, and a bigger one just uses more memory and adds latency (longer backlog) before overflowing anyway. The real fix is backpressure (control the producer) or more consumer capacity.

When is dropping data acceptable instead of backpressure?

When freshness beats completeness and stale data is worthless — live sensor readings, metrics, video frames, best-effort logs — dropping the oldest/excess keeps latency low (load shedding is the deliberate version). When every item matters (payments, orders), dropping is unacceptable; use backpressure plus durable buffering.

How does TCP implement backpressure?

Via the flow-control window: the receiver advertises its free buffer space (receive window) in each ack, and the sender can’t have more unacknowledged data in flight than that. A slow receiver advertises a smaller window, throttling the sender — backpressure end to end. (This is distinct from congestion control, which reacts to the network.)

Keep going

Finished this one? 0 / 44 Labs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Labs