Knowledge Guide
HomeSystem DesignMessaging

hard Back-Pressure Mechanisms: block, drop, or shed

Mechanism: back-pressure is the missing feedback loop

A producer and a consumer sit on either end of a queue. If the producer emits faster than the consumer drains, the gap between them has to go somewhere — and if nothing constrains it, it goes into the queue, which grows without limit. An unbounded queue does not fail loudly; it fails by silently trading memory for time: resident memory climbs, GC pressure rises, and every item's wait-in-queue time keeps growing until the process is OOM-killed or the tail latency becomes unusable — unbounded lag. Back-pressure is the feedback signal that tells the producer “slow down” before that happens, and it only works if the buffer between them is bounded — because a bound is what forces the system to decide, the instant it fills, what happens next.

Once the buffer is bounded, there are exactly three things you can do the moment it fills:

  1. Block the producer (flow control) — the enqueue call itself stalls until a slot frees up. No item is lost; the slowness is pushed upstream, all the way back to whoever originally requested the work. Examples: a bounded BlockingQueue.put(), TCP's receive window (the sender stops when the advertised window hits zero), Reactive Streams backpressure.
  2. Drop (load-shed) — reject or discard instead of enqueueing, and tell the caller so (HTTP 429/503, a NACK). Memory and latency stay bounded because the queue never grows past its cap; the cost is that some work never gets done.
  3. Sample / degrade — keep accepting everything but do less per item (coarser aggregation, a lower-fidelity response, skip non-essential fields). A softer cousin of dropping: you shed cost per item instead of shedding whole items.

Credit-based flow control: making "block" explicit instead of reactive

A plain bounded queue only signals back-pressure after it is already full — that is reactive, and by the time the producer notices, a full buffer's worth of items are already sitting in line. Credit-based flow control (Reactive Streams' request(n), gRPC and HTTP/2 flow-control windows) makes the demand explicit instead: the consumer grants the producer a credit of N items it can currently absorb; the producer may send at most N and must then stop; as the consumer finishes processing items, it refills the credit. The producer never has to guess or overshoot into a full buffer — it only ever sends what the consumer has already said it can take. This is still "block" in spirit (no loss, upstream propagation) but the signal arrives one round-trip earlier and is explicit rather than inferred from a stall.

Traced: 10,000/s in, 6,000/s out, a queue of 10,000

Take a producer emitting 10,000 messages/second into a queue bounded at 10,000 slots, drained by a consumer that can only process 6,000/second. The queue grows by the difference every second:

quantityvalue
produced10,000/s
consumed6,000/s
net growth10,000 − 6,000 = 4,000/s
time to fill cap 10,00010,000 / 4,000 = 2.5s

So 2.5 seconds after the burst starts, the queue is completely full and the system must apply one of the two hard responses below.

Producer at 10,000 msg/s feeding a bounded queue of capacity 10,000, drained by a consumer at 6,000 msg/s. A timeline shows queue depth rising 0, 4000, 6000, 8000, hitting 10,000 (FULL) at t=2.5s. From there two branches: BLOCK (flow control) caps accepted rate at 6,000/s, adds about 1.7s queueing delay, no items lost, but risks stalling a caller thread; DROP (load shedding) keeps the queue pinned at cap, accepts 6,000/s and sheds 4,000/s (40%) returning 429/503, bounded latency but lost work.
Producer at 10,000 msg/s feeding a bounded queue of capacity 10,000, drained by a consumer at 6,000 msg/s. A timeline shows queue depth rising 0, 4000, 6000, 8000, hitting 10,000 (FULL) at t=2.5s. From there two branches: BLOCK (flow control) caps accepted rate at 6,000/s, adds about 1.7s queueing delay, no items lost, but risks stalling a caller thread; DROP (load shedding) keeps the queue pinned at cap, accepts 6,000/s and sheds 4,000/s (40%) returning 429/503, bounded latency but lost work.

What happens at t = 2.5s: the two paths diverge

Block (flow control): the producer's next enqueue call stalls because there is no free slot. From this instant, the pipeline's accepted throughput is capped at the consumer's drain rate — 6,000/s, no faster, no matter how hard the producer pushes. Anything already queued now waits queue depth ÷ drain rate ≈ 10,000 / 6,000 ≈ 1.7 additional seconds before it is even looked at. If the thing calling the producer is itself a user-facing request handler, that caller now blocks for that same 1.7s+ on top of normal processing time. No item is lost — every one of the 10,000/s offered is eventually processed — but the slowness has propagated one hop upstream.

Drop (load-shed): the queue is held at its cap; the system keeps accepting only what the consumer can actually drain (6,000/s) and immediately rejects the remaining 4,000/s — 40% of offered load — typically with an HTTP 429/503 so the caller knows to back off or retry elsewhere. Queue depth and therefore latency for anything that is accepted stay flat and bounded, because the buffer never grows past 10,000. The cost is explicit: 4,000 requests/second get no work done at all unless the caller retries.

Pitfalls

The judgment layer: block vs drop vs "just make the buffer bigger"

Block / flow control is the right call inside a pipeline you control end-to-end, where correctness matters more than latency and there is no user sitting on the other end of the stalled call — internal ETL stages, a Kafka consumer group falling behind its partition, TCP itself. You get zero loss, but you must be sure the block cannot deadlock (producer and consumer must not be waiting on each other in a cycle) and cannot be felt by an unrelated caller.

Drop / load-shed is the right call at a user-facing edge — an API gateway, a load balancer, an ingress queue — where keeping the system alive and responsive for the requests it does accept matters more than completing every single request. You trade completeness for a hard, predictable bound on memory and latency; this only works if dropped requests are safe to lose or the caller is expected to retry (idempotently) elsewhere.

"Just make the buffer bigger" is not a genuine third option — it is the same unbounded-queue failure mode with the crash pushed further out. A bigger cap raises the ceiling on both memory and worst-case queueing latency (every accepted item now waits behind a longer line before it is even seen), so it trades a later, larger failure for worse latency in the meantime. Size the buffer to absorb real, short-lived bursts you've measured — not as a substitute for choosing block or drop.

Takeaways


Synthesized from TCP flow-control (receive-window semantics), the Reactive Streams specification, gRPC/HTTP2 flow-control windows, the Google SRE Book's treatment of load shedding, and Nygard's Release It! on bulkheads and cascading failure. Re-authored/Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on Back-Pressure Mechanisms: block, drop, or shed? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Back-Pressure Mechanisms: block, drop, or shed** (System Design) and want to truly understand it. Explain Back-Pressure Mechanisms: block, drop, or shed from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Back-Pressure Mechanisms: block, drop, or shed** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Back-Pressure Mechanisms: block, drop, or shed** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Back-Pressure Mechanisms: block, drop, or shed** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes