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
| Policy | What happens | Preserves all work? | Bounded memory? |
|---|---|---|---|
| Unbounded | Always enqueue | Yes (until OOM) | No |
| Block producer | Wait until space | Yes | Yes |
| Reject | Fail fast to caller | No (caller retries or gives up) | Yes |
| Drop newest | Discard the new item | No | Yes |
| Drop oldest | Evict head, take new | No | Yes |
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.
| Policy | Processed | Dropped / Rejected | Producer stalls | Max queue |
|---|---|---|---|---|
| Unbounded | 5,000 | 0 / 0 | 0 | 3,750 |
| Drop oldest | 1,314 | 3,686 / 0 | 0 | 64 |
| Drop newest | 1,314 | 3,686 / 0 | 3,686 | 64 |
| Reject | 1,314 | 0 / 3,686 | 3,686 | 64 |
| Block producer | 5,000 | 0 / 0 | 14,744 | 64 |
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.
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
| Situation | Prefer | Why |
|---|---|---|
| Every message must be processed | Block (or bounded + durable queue) | No silent loss |
| Interactive API, client can retry | Reject (429 / 503) | Fail fast; client backs off |
| Metrics / telemetry samples | Drop oldest or newest | Freshness > completeness |
| ”Just use a bigger queue” | Still set a max | Unbounded is a time bomb |
What breaks? — Anti-patterns
Unbounded in-memory queues “for throughput.” Throughput looks great until GC / OOM. Fix: hard max + explicit policy.
Retries into a full system without delay. Day 37 without backpressure = self-DDoS. Fix: reject + jittered backoff; stop retrying on 429.
Dropping without metrics. Silent data loss is worse than a loud reject. Fix: count drops/rejects; alert on rate.
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
- A websocket fan-out is 10× faster than a mobile client. Unbounded buffer or drop-oldest? Why?
- How does HTTP/2 flow control express backpressure compared to our “block producer” sim?
- 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.