skip to content

Between the stages of a concurrent processing chain you can place an unbounded queue or a fixed-capacity one. Which do you choose, how do you pick the capacity, and what happens to the neighbouring stages when that queue is full or empty?

level: seniorimportance: must knowfreq 48%

answer

  1. Bounded always; unbounded hides the bottleneck
  2. Full → upstream blocks; empty → downstream starves
  3. Depth = latency (depth ÷ throughput)
  4. Batch k items or t ms, whichever first
  5. Cycles + bounded queues = deadlock

basics

~20 s

Always bounded. An unbounded queue turns a rate mismatch into unbounded memory and unbounded latency. Bounded means a full queue blocks the upstream stage and an empty queue stalls the downstream one — both visible, self-limiting signals. Size it to cover normal jitter only, since depth becomes latency.

solid answer

~60 s

**Bounded, essentially always.** An unbounded queue silently absorbs a permanent rate mismatch: memory grows, latency grows linearly with uptime, and the failure surfaces late as an out-of-memory kill. A bound converts that into an immediate, local, observable symptom. With a bound there are two states worth naming: - **Full** — the upstream stage blocks on put. That is pressure propagating backwards, one hop at a time, until it reaches the entry point where a policy can be applied. - **Empty** — the downstream stage blocks on take. It is **starved**; the pipeline has a bubble and capacity is wasted. Sizing: big enough that the next stage rarely starves during normal jitter (roughly burst size, or arrival-rate × the duration of a typical hiccup), small enough that depth × (1/throughput) fits the latency budget — depth is latency by Little's law. Tens to a few hundred slots are typical; thousands usually means someone is hiding a bottleneck. Watch for deadlock: bounded queues plus any cycle, or one worker that both feeds and drains a bounded queue, can wedge the whole chain.

code

text · 5 lines
text
queue capacity = 4

[####]  full   -> producer blocked on put()   (upstream stall)
[##..]  healthy-> both stages progress
[....]  empty  -> consumer blocked on take()  (bubble / starvation)

go deeper

for a junior

Know that queues between stages should be bounded, and that full means the producer waits while empty means the consumer waits.

for a middle

Explain the memory-and-latency argument against unbounded queues and size the buffer to cover jitter rather than a permanent mismatch.

for a senior

Derive capacity from a latency budget, read queue-depth signatures to locate the bottleneck, cover batching with a flush timeout, and name the deadlock and shutdown hazards.

for a principal

Set pipeline-wide in-flight limits from a service-level target, decide the policy at the entry point when pressure arrives there, and design the stage graph (acyclic, isolated work classes, ordered drain) so failure modes stay contained.

## Why bounded A queue between two stages exists to decouple them so a brief slowdown in one does not immediately idle the other. It does **not** exist to reconcile a permanent rate difference — nothing can do that except making the slow stage faster or the fast stage slower. If the producing stage is persistently faster than the consuming stage, an **unbounded** queue grows forever. Three bad consequences: memory grows until the process dies; per-item latency grows linearly with elapsed time (an item entering after an hour of imbalance waits an hour); and the system looks *healthy* the whole time — every worker busy, no errors — so the true fault (a bottleneck) is invisible until a hard crash. Worse, at crash time you lose everything buffered. A **bounded** queue makes the mismatch visible instantly and locally: the queue sits at capacity, the upstream stage spends its time blocked, and the queue-depth metric points straight at the constraint. Memory is capped by design, and latency is capped at depth ÷ throughput. ## Full and empty: the two stall modes - **Full queue → upstream stalls.** The producing stage blocks when it tries to hand off. It stops consuming from its own inbox, which fills, which blocks the stage before it. This backward propagation is the pipeline's self-regulation: within one round trip the whole chain runs at the bottleneck's rate, and pressure arrives at the source, which is the only place that can meaningfully decide what to do about excess work (block the caller, shed, spill, slow the poll). Choosing among those overflow policies is a separate design topic; the pipeline-internal mechanism is simply that a bounded handoff blocks. - **Empty queue → downstream stalls.** The consuming stage has nothing to do — a **bubble**. Bubbles are pure lost capacity: the bottleneck idling for 10% of the time costs 10% of maximum throughput and cannot be recovered later. Causes include an upstream stage with high variance (an occasional slow item), buffers too small to cover that variance, expensive handoffs, and start-of-stream fill. A healthy pipeline shows queues that oscillate: partially full, occasionally empty. Persistently full means the downstream stage is the constraint; persistently empty means the upstream stage (or the source) is. ## Choosing the capacity Two forces pull in opposite directions: - **Lower bound (avoid bubbles).** The buffer must cover normal variation. If the producer occasionally pauses for t and the consumer serves at rate r, roughly r · t items of buffer keep the consumer fed. Batchy producers need at least one batch of room. - **Upper bound (latency).** By Little's law the delay a full queue adds is depth ÷ throughput. At 100 items/s, a 1000-slot queue adds 10 seconds. Derive the maximum from the latency budget: max in-flight = target latency × throughput, then divide that allowance across the stages. In practice, start small (tens), measure starvation (time the consumer spends blocked on empty) and latency, and grow only if starvation is real. A large buffer that never drains is a bottleneck in disguise. ## Batching When per-item handoff cost is significant relative to per-item work, move items in **batches**: one lock acquisition, one wakeup, one network or disk operation for k items. Batching raises throughput (amortized overhead, better cache and I/O behaviour) and is often the difference between a pipeline that helps and one that is all overhead. The costs: latency grows by up to a batch-fill time, and a partly filled batch must be flushed on a timeout or it can sit forever at low load — so batching policies are always "k items **or** t milliseconds, whichever first". Batch sizing is the same tradeoff as buffer sizing: bigger batch, more throughput, more latency, more work lost or replayed on failure. ## Deadlock hazards specific to bounded buffers Bounding introduces blocking, and blocking introduces deadlock: - **Cycles.** If a later stage feeds work back to an earlier one over a bounded queue, both can block on full queues waiting for each other. Break it with a cycle-free graph, a dedicated monitored feedback path, or non-blocking offer-with-drop on the back edge. - **One worker on both sides.** A worker that drains queue A and pushes to queue B must never be the only worker that also drains B; if B fills, it blocks and can never drain B. - **Fan-out/fan-in join.** A stage that must take one item from each of two bounded inputs deadlocks if one input's producer is blocked behind the other's full queue. - **Shutdown.** Draining requires ordered termination: stop the source, send end-of-stream markers downstream, let each stage finish and forward the marker. Killing a middle stage while its neighbours block on it wedges the chain.

  • Your queues between stages are configured at 100,000 and are always near full. What does that tell you and what do you do?
    It says the downstream stage is the bottleneck and the buffer is hiding it: the queue is not absorbing bursts, it is storing a permanent backlog, and it is adding depth ÷ throughput of latency to every item. The fix is to raise the bottleneck's capacity or reduce its work, and then shrink the queue to a size that only covers jitter so the mismatch becomes visible immediately next time.
  • How can bounded buffers deadlock a pipeline that contains no locks at all?
    Blocking on a full or empty queue is itself a wait-for edge. If the stage graph has a cycle — a later stage feeding results back to an earlier one — both stages can end up blocked putting into each other's full queues. The same happens when one worker is responsible for both draining and filling the same bounded queue, or when a join stage waits for one input while its producer is blocked behind the other. Keep the graph acyclic, or use non-blocking offers with a drop or spill policy on back edges.
  • When does batching between stages hurt?
    When latency matters and load is low: a batch of k waits for k arrivals, so at low rates items sit until the flush timeout fires, which is why every batching policy needs a time bound as well as a size bound. Batching also enlarges the blast radius of a failure — a lost or retried batch affects k items — and it makes per-item progress lumpier, which can worsen tail latency even while raising average throughput.

A conveyor with room for ten parts between two workstations: if the second station is slower the belt fills and the first must stop, which everyone can see. An infinitely long belt just piles parts up until the warehouse floor gives way.

saying these in an interview costs you the question

  • Preferring an unbounded queue 'so nothing ever blocks' — it just moves the failure to memory
  • Sizing buffers large to 'increase throughput' when throughput is fixed by the slowest stage
  • Not distinguishing a full queue (downstream is the constraint) from an empty one (upstream is)
  • Adding a feedback edge with bounded blocking queues and not seeing the deadlock
  • Batching with only a size trigger and no flush timeout, so items stall at low load

context