skip to content

After a team reorganized a request handler into a chain of concurrent stages with queues between them, total requests per second went up but the time an individual request takes got worse. Explain why that is the expected outcome and how you would reason quantitatively about the per-item time.

level: seniorimportance: should knowfreq 45%

answer

  1. Throughput technique, not latency technique
  2. W = L / λ (Little's law)
  3. Buffer depth = latency, not capacity
  4. Wait grows as ρ/(1−ρ) near saturation
  5. Measure service time and queue wait separately

basics

~20 s

Pipelining raises throughput by keeping several items in flight, but one item still visits every stage in order and now also pays handoff and queue waiting. Per-item time = sum of stage service times + waiting. Little's law gives the wait: average time in system = items in system / throughput.

solid answer

~50 s

Pipelining is a **throughput** technique, not a latency technique. A single request still traverses every stage sequentially, so its service time is unchanged at best; on top of that it now pays per-stage handoff (enqueue, wakeup, cache-cold data on another core) and, crucially, **waiting in buffers**. Quantitatively, use **Little's law**: L = λ · W, where L is the average number of items in the system, λ the throughput, and W the average time in system. Rearranged, W = L / λ. If the pipeline holds 200 items across its queues and drains 100/s, mean latency is 2 s regardless of how fast the code is. Deep queues in front of the bottleneck are latency, not capacity. So: bounded, small buffers to cap the wait; measure per-stage service time and per-stage queue wait separately; note that latency is worst at the bottleneck's inbox. If per-request latency is the actual goal, you need less work per request or intra-request parallelism — a deeper pipeline makes it worse.

code

text · 4 lines
text
target latency W = 0.2 s
measured throughput  = 100 items/s
max items in flight L = W * lambda = 0.2 * 100 = 20
=> total capacity of ALL queues + in-service slots <= ~20

go deeper

for a junior

Say clearly that one item still passes through every stage in order, plus new handoff and waiting costs, so throughput and latency are different things.

for a middle

Bring in Little's law (W = L/λ) and the point that deep buffers are latency, not capacity.

for a senior

Diagnose: split per-stage service time from queue wait, use utilization to explain the tail, then shrink buffers or add bottleneck capacity.

for a principal

Set the target explicitly — which metric the product is actually buying — and derive buffer and concurrency limits from a latency budget, including work-class isolation and load-shedding policy.

## Two different metrics **Throughput** is items completed per unit time (λ). **Latency** is the time one item spends from entry to exit (W). They are related but not interchangeable, and pipelining moves them in opposite directions. A pipeline raises throughput because several items are in flight at once, one per stage, so all the workers are busy simultaneously. It does nothing for a single item, which still visits stage 1, then 2, then 3, in exactly the order it would have visited them in a sequential implementation. Its best case latency is the same sum of service times as before. ## Why latency gets actively worse Three additions: 1. **Handoff cost.** Each stage boundary means enqueue, dequeue, and usually a worker wakeup, plus a scheduling delay before the next stage's worker is actually running. The item's data also migrates between cores, so the next stage starts with cold caches. For stages that are individually short, handoff can be a large fraction of the stage. 2. **Queue waiting.** An item that arrives while the next stage is busy waits. Waiting time dominates everything else once the bottleneck approaches saturation. 3. **Variance amplification.** Any burst in arrivals or any slow item at the bottleneck delays every item queued behind it — head-of-line blocking. This shows up as a much worse p99 than p50, even when average throughput looks excellent. ## Little's law: the quantitative handle **L = λ · W** — for any stable system, the average number of items inside equals arrival (= completion) rate times average time inside. It assumes nothing about distributions, only stability (nothing accumulating forever). Applied to a pipeline: - Whole pipeline: if 200 items are resident across all queues and stages and it completes 100 items/s, mean latency is W = 200/100 = **2 seconds**. If your latency budget is 200 ms at 100/s, you may have at most 20 items in flight — that is a hard cap on total buffer capacity, and it is the principled way to size queues. - Per stage: a stage serving 20 items/s with a queue averaging 40 deep contributes 2 s of waiting on its own. Sum the per-stage L/λ values to see which stage owns the latency. The useful reframing: **buffer depth is latency**. Adding queue capacity in front of a saturated stage does not add capacity; it converts a would-be rejection or upstream block into seconds of waiting. Throughput is set by the bottleneck's service rate and nothing else. ## Utilization makes it non-linear As a stage's utilization ρ (arrival rate ÷ service rate) approaches 1, queue wait grows roughly like ρ/(1−ρ) for random arrivals. At 50% utilization the wait is about one service time; at 90% it is nine; at 99% it is ninety-nine. This is why a pipeline that is "fully utilized" has terrible latency and why latency-sensitive systems deliberately run the bottleneck stage well below saturation. It also explains an observed cliff: raising load from 80% to 95% of capacity barely improves throughput but multiplies latency. ## How to diagnose Instrument each stage with two separate numbers: **service time** (how long the stage held the item) and **queue wait** (how long it sat in the inbox). The sum across stages is the end-to-end latency, and the split immediately says whether the fix is faster code or less queueing. Also record queue depth over time; a queue whose depth is high and stable is pure added latency, and its depth divided by throughput is exactly the delay it contributes. ## What to do about it - **Shrink buffers** to the smallest size that keeps the bottleneck fed through normal jitter. Latency falls immediately; throughput is unaffected as long as no stage starves. - **Raise bottleneck capacity** (replicate or make it cheaper) so utilization drops and waiting collapses. - **Merge trivially short stages** so handoff cost stops dominating. - **Separate classes of work** — a latency-critical path should not queue behind a bulk path in the same buffer. - **Accept the tradeoff explicitly.** If per-request latency is the product requirement, pipelining is the wrong tool: reduce per-request work, or overlap the request's own steps if they are independent. If total system rate is the requirement, pipelining plus small buffers is right and you defend the latency number with Little's law.

  • Throughput is fine but p99 latency is ten times p50. Where do you look first?
    At queue wait rather than service time, and specifically at the bottleneck stage's inbox. Near-saturated stages have wait times that explode non-linearly (roughly ρ/(1−ρ)), so a modest burst puts many items behind one another and tail latency blows up while the average still looks fine. Look for head-of-line blocking from occasional slow items and for buffers deep enough to hide the problem, then reduce buffer depth or add bottleneck capacity.
  • Does increasing the queue size between stages ever help throughput?
    Only when a stage would otherwise starve or block due to burstiness or jitter — a small buffer smooths a producer that emits unevenly so the next stage never sits idle. Beyond the size needed to cover that jitter, extra depth adds nothing to the drain rate, because throughput is fixed by the slowest stage's service rate; it only adds waiting time and memory.

A supermarket adding more checkout lanes serves more shoppers per hour, but joining a long line still means a long wait — and a longer line means a longer wait, not a faster store.

saying these in an interview costs you the question

  • Claiming a pipeline reduces per-request latency because 'work runs in parallel'
  • Adding queue capacity to fix a throughput problem
  • Treating high utilization of the bottleneck as unambiguously good, ignoring the latency cliff
  • Quoting only average latency and never the tail
  • Not distinguishing service time from queue wait when measuring

context