Skip to content
← Back to all lessons
Day 044 System Design

Backpressure - teach the producer to slow down

Fast producer, slow consumer. Unbounded max depth 3,750 vs cap 64 (58.6x). Block keeps all work; reject fails fast.

7 min read

This video presents visual lesson highlights with instrumental background music. The complete lesson is available as text below.

Day 42 bulkheads isolate pools. Day 35 breakers stop calling a sick dependency. Day 37 retries try again later. Today: when the producer is simply faster than the consumer, unbounded queues turn lag into an OOM. Backpressure is the signal that propagates “enough.”

Backpressure is feedback from a slower consumer (or full buffer) that forces the producer to wait, drop, or reject — instead of growing memory without bound.

The problem

A producer emits 4× faster than a consumer can process. With an unbounded queue, depth climbs until the process dies. With a capacity of 64, you must choose a policy when the queue is full.

// shape of the trap
while (true) {
  queue.push(produce()); // never checks depth
  // consumer somewhere else: queue.shift()
}

Five policies when the buffer is full

PolicyWhat happensPreserves all work?Bounded memory?
UnboundedAlways enqueueYes (until OOM)No
Block producerWait until spaceYesYes
RejectFail fast to callerNo (caller retries or gives up)Yes
Drop newestDiscard the new itemNoYes
Drop oldestEvict head, take newNoYes

Lab — Node sim, 30-run median

Produce 5,000 items. Consumer drains 1 item every 4 producer ticks (4× slower). Capacity 64. The simulation script and standalone write-up are not included in this repository.

PolicyProcessedDropped / RejectedProducer stallsMax queue
Unbounded5,0000 / 003,750
Drop oldest1,3143,686 / 0064
Drop newest1,3143,686 / 03,68664
Reject1,3140 / 3,6863,68664
Block producer5,0000 / 014,74464

Headlines: unbounded max depth 3,750 vs cap 64 → 58.6× over capacity (memory-bomb shape). Block keeps max queue at 64 and still processes all 5,000 — producer pays in stalls. Reject/drop process only ~1,314 and surface pressure immediately.

max queue depth · cap = 64unbounded3,750cap 64block64reject64 · 3686 rejected

Where you have seen this already

  • Day 9 rate limit: admit control at the edge — a form of backpressure on clients.
  • Day 14 queues: a queue without a max size is an unbounded buffer by default.
  • Day 35 circuit breaker: stops calling a sick dependency (failure isolation), not the same as slowing a healthy but slow consumer.
  • Day 37 retry + jitter: retries without backpressure amplify load (thundering herd).
  • Day 42 bulkhead: caps concurrent work per compartment; full compartment should reject or queue with a bound.

Reactive Streams one-liner

The Reactive Streams spec exists to standardize non-blocking backpressure: a subscriber requests n items; the publisher must not push more than requested. Same idea as our “block” / “request more” — but async.

Worked walk — one full buffer moment

cap = 64 · queue already full · producer has item #2000

block:   wait tick → consumer frees 1 → enqueue #2000 (stall++)
reject:  return 503 to caller; item never enters queue (rejected++)
drop_new: discard #2000; queue unchanged (dropped++)
drop_old: pop #1936, push #2000 (dropped++; fresher window)
unbounded: push #2000; depth becomes 65, 66, … toward 3,750

Interview voice: “I pick the policy from the product: money transfers block or durable-queue; metrics drop; public APIs reject with Retry-After.”

HTTP shape of reject

HTTP/1.1 429 Too Many Requests
Retry-After: 2
// or 503 Service Unavailable when the worker pool is saturated

Reject without a signal teaches clients to hammer harder. Pair with Day 37 jitter on the client, and stop retrying when the server says stop.

Decision table — pick a policy

SituationPreferWhy
Every message must be processedBlock (or bounded + durable queue)No silent loss
Interactive API, client can retryReject (429 / 503)Fail fast; client backs off
Metrics / telemetry samplesDrop oldest or newestFreshness > completeness
”Just use a bigger queue”Still set a maxUnbounded is a time bomb

What breaks? — Anti-patterns

  1. Unbounded in-memory queues “for throughput.” Throughput looks great until GC / OOM. Fix: hard max + explicit policy.

  2. Retries into a full system without delay. Day 37 without backpressure = self-DDoS. Fix: reject + jittered backoff; stop retrying on 429.

  3. Dropping without metrics. Silent data loss is worse than a loud reject. Fix: count drops/rejects; alert on rate.

  4. Confusing bulkhead full with backpressure. Bulkhead limits concurrency; backpressure is the signal when the limit is hit. Fix: full pool → reject or block with timeout, not infinite wait.

How it connects

  • Day 42 (Bulkhead): compartments cap resources; backpressure is what you do when a compartment is full.
  • Day 35 (Circuit breaker): open circuit is fail-fast toward a bad dependency; backpressure can target a healthy slow path.
  • Day 37 (Retry jitter): retries must respect pressure signals or they fight the consumer.
  • Day 9 / 14: rate limits and queues are the usual places to implement the policy.

Transfer questions

  1. A websocket fan-out is 10× faster than a mobile client. Unbounded buffer or drop-oldest? Why?
  2. How does HTTP/2 flow control express backpressure compared to our “block producer” sim?
  3. Your queue max is 10,000 and p99 lag is climbing. Which metric proves you need a stricter policy, not a bigger max?

Quiz

1. Unbounded queue vs capacity 64 in our lab — max depth?

2. Which policy keeps all work and bounds memory?

3. Day 42 bulkhead vs Day 44 backpressure?

Your teach step

Close this lesson. Write the “Explain like I’m 10” and the “60-second LinkedIn version” from memory. Focus on: slow down the producer; 58.6× depth bomb; block vs reject vs drop; compose with bulkhead/breaker/retry. Post it, and paste the link.

Questions? Ask the agent — TCP windowing, Kafka consumer lag, or HTTP 429 design.