Queueing Theory for SRE
Ask an engineer why latency exploded when traffic crept from 70% to 90% of a service's rated capacity, and the honest answer is rarely "the code got slower." It's that queueing delay is not proportional to utilization — it's a curve that stays nearly flat for most of its range and then turns almost vertical right where most capacity plans quietly assume it's still safe. This page is the math underneath that curve: Little's Law, the one identity that ties arrival rate, time-in-system, and queue length together with no assumptions attached; the M/M/1 model that shows exactly why the relationship between utilization and delay has a (1 − ρ) sitting in the denominator; and a worked, numeric comparison of 70% and 90% utilization on the same service that turns "the tail-latency cliff" from a phrase into an actual number. By the end you'll know why a capacity plan built entirely on average CPU utilization is measuring the wrong thing.
Picture a highway lane that can comfortably fit 10 cars a minute passing a certain point. At 7 cars a minute, traffic flows freely — everyone's happy, there's space between cars to absorb a little braking. At 9 cars a minute, it feels like it should be almost the same story — just two more cars — but now there's no space left to absorb anything. One driver taps their brakes, a truck merges, and instead of a tiny ripple that fades out, you get a stop-and-go traffic jam that takes ages to clear. The road didn't get "a little worse." It got dramatically, disproportionately worse, because those last two cars used up the room the system needed to absorb the ordinary hiccups it was always going to have. That's the whole idea behind queueing theory: past a certain point, a small increase in how full a system is causes a wildly out-of-proportion increase in how long everything takes to get through it — and the math that describes exactly where that point is and why is what this page is about.
Why 80% looked safe, and wasn't
☺ Like you're 10: The mystery this page solves: a dashboard that looked fine the whole time a service was quietly falling apart, and the reason isn't a monitoring bug — it's a curve nobody was looking at.
Here is a scenario that repeats, with minor variations, across nearly every production incident retro that ends with the words "capacity issue." A fleet has been running at a comfortable, unremarkable 65–75% average CPU utilization for weeks. Traffic grows the way traffic always grows — a marketing push, an organic trend, a new integration partner sending more requests — and average utilization creeps up to 85%, then 90%. Nobody's especially alarmed; it's still "under 100%," and the team has headroom charts that show plenty of green. Then, often within a single afternoon, p99 latency stops looking like a noisy metric that occasionally spikes and starts looking like a different service entirely — 20ms responses becoming 2-second responses, timeouts cascading into retries, retries adding more load to an already-saturated system. The postmortem calls it a capacity issue, which is true, but incomplete: the actual cause is that the relationship between utilization and delay is not a straight line, and nobody on the team had internalized the shape of the curve they were standing on.
That curve isn't folklore or intuition — it's the literal output of a branch of applied probability built to answer exactly this question, starting with a Danish telephone engineer sizing switchboards in 1917 and formalized into the identity every capacity plan implicitly leans on, proven by an MIT professor in 1961. This page works through both pieces: the identity that always holds regardless of how bursty or well-behaved your traffic is (Little's Law), and the specific model that shows why the curve has the shape it does and gets steep exactly where a naive capacity plan assumes it's still safe (the M/M/1 model). Then we do the arithmetic Sol would insist on: a real, worked comparison of 70% and 90% utilization on the same service, and an honest look at why an average-utilization dashboard is structurally incapable of warning you before the cliff, not just unlucky.
Little's Law: the one identity that always holds
☺ Like you're 10: No matter how weird or bursty your traffic is, one simple equation about a queue is always, exactly true — and it's the closest thing capacity planning has to a law of physics.
In 1961, John D. C. Little, then at MIT, published a short proof in Operations Research titled "A Proof for the Queuing Formula: L = λW," and the identity it proved is still the single most load-bearing fact in this entire page. Little's Law states that for any stable queueing system — one where, on average, work leaves at the same rate it arrives, so the queue isn't growing without bound — the average number of items in the system equals the average arrival rate multiplied by the average time each item spends in the system:
L = λ · W
L = average number of requests "in the system" right now
(waiting in a queue + currently being served)
λ = average arrival rate (requests per second)
W = average time a request spends in the system,
end to end (queueing delay + actual service time)What makes this remarkable, and worth pausing on, is everything the proof does not require. It doesn't assume Poisson arrivals. It doesn't assume any particular service-time distribution — exponential, deterministic, wildly irregular, it doesn't matter. It doesn't assume a scheduling discipline — first-in-first-out, priority queues, last-in-first-out, all fine. It doesn't even require a single server; "the system" can be one instance, an entire fleet, a database connection pool, or a whole distributed pipeline, as long as it's stable in the sense above. Little's Law is a conservation law, in the same spirit as conservation of mass: whatever units of work go in eventually come out, and that constrains the relationship between how many are in flight, how fast they arrive, and how long each one stays — regardless of the messy details of what happens to them while they're in there.
Using it in reverse: sizing from a target
Little's Law is most often stated as "measure λ and W, compute L," but SREs use it just as often the other way around: pick a target throughput and a target latency, and let it tell you the minimum concurrency the system needs to sustain them. Suppose a checkout API needs to sustain 500 requests/second, and the SLO commits to keeping response time around 40 ms on average. Little's Law says the fleet must be able to hold, on average, L = λ × W = 500 × 0.04 = 20 requests in flight at once — a floor on total concurrency (thread-pool slots, event-loop pending callbacks, database connections in active use) the system needs, end to end, to hit that target without a growing backlog. Note the word floor: this number is the bare minimum implied by the target, not a safe number to provision exactly at, and the next two sections are entirely about why. This same reversed formula returns in the capacity-planning-policy section as the basis for a concrete admission-control rule.
Where the ρ/(1−ρ) curve comes from: the M/M/1 model
☺ Like you're 10: Little's Law tells you the pieces are related. This is the specific formula that tells you exactly how ugly things get as a queue fills up — and why "ugly" arrives suddenly, not gradually.
Little's Law holds for any stable queue, which is exactly why it can't, on its own, tell you the shape of the relationship between utilization and delay — for that you need a model with actual assumptions attached. The simplest and most-cited one uses Kendall's notation, shorthand of the form Arrival/Service/Servers: M/M/1 means Markovian (memoryless) arrivals — a Poisson process, the standard model for many independent, uncoordinated requesters — Markovian (exponential) service times, and one server. It's a toy model in the sense that real production traffic is rarely exactly Poisson (more on that in the next section), but it's the toy model that produces the exact curve every more realistic model is a variation of, so it's worth deriving precisely once.
The formulas, and the one thing that matters about them
Define utilization as ρ = λ/μ, where λ is the average arrival rate and μ is the maximum sustainable service rate of the single server (so μ = 1 / average service time). For a stable M/M/1 queue (ρ < 1), the classical results — derivable from the birth-death process underlying the model — are:
ρ = λ / μ utilization (fraction of capacity in use)
L = ρ / (1 − ρ) average number of requests in the system
Lq = ρ² / (1 − ρ) average number waiting in queue (not being served)
W = (1/μ) / (1 − ρ) average time in system = 1 / (μ − λ)
Wq = ρ · (1/μ) / (1 − ρ) average time waiting in queue = ρ / (μ − λ)Every one of those four quantities shares the same denominator: (1 − ρ). That single term is the entire story of this page. As ρ climbs from 0 toward 1, (1 − ρ) shrinks toward 0, and because it's in the denominator, every formula above grows without bound as ρ approaches 1 — not linearly, not even quadratically in the region that matters most, but hyperbolically. At ρ = 0.5, 1/(1−ρ) = 2. At ρ = 0.9, it's 10. At ρ = 0.99, it's 100. The gap between "half-full" and "nearly full" isn't twice as bad — by this measure it's fifty times as bad, and the worked numbers in the next section make that concrete rather than abstract.
Capacity headroom isn't padding you add for comfort or to satisfy a finance review — it's literally the (1 − ρ) term sitting in the denominator of every queueing formula on this page. The closer a system runs to ρ = 1, the smaller that denominator gets, and the more explosively any given formula's output grows. "How much headroom do we need" and "how far are we from the vertical part of this curve" are the same question asked two different ways.
Worked example: 70% vs. 90% utilization on the same service
☺ Like you're 10: Same service, same code, same hardware — just 20 more percentage points of traffic — and here's exactly how much worse it gets, in real milliseconds, not just in theory.
Take a single instance of a service whose average request takes 5 ms to handle when nothing else is queued — a fast, cache-backed read path — saturating at μ = 200 requests/second. Applying the M/M/1 formulas from the previous section at ρ = 0.7 and ρ = 0.9:
baseline service time = 1/μ = 5 ms μ = 200 req/s
At ρ = 0.7 (λ = 140 req/s):
W = 5 ms / (1 − 0.7) = 5 / 0.3 = 16.7 ms average total latency
Lq = 0.7² / 0.3 = 1.63 avg requests waiting (not yet served)
At ρ = 0.9 (λ = 180 req/s):
W = 5 ms / (1 − 0.9) = 5 / 0.1 = 50.0 ms average total latency
Lq = 0.9² / 0.1 = 8.1 avg requests waiting (not yet served)
Traffic increase, 70% → 90% utilization: 140 → 180 req/s = +28.6%
Mean-latency increase, same move: 16.7 → 50.0 ms = ×3.0A 28.6% increase in traffic produces a 3× increase in mean latency. That asymmetry — more traffic than latency for most of the curve, then dramatically less traffic than latency once you cross the knee — is the entire "why did latency explode" mystery, expressed as arithmetic instead of a war story.
The tail is worse than the mean, and it's worse by a knowable amount
M/M/1 has a second, less commonly quoted result that matters more operationally than the mean: because of the model's memoryless assumptions, the total time a request spends in the system is itself exponentially distributed, with rate (μ − λ) — the same rate that appears in the mean-latency formula, since the mean of an exponential distribution is 1/rate. That gives a clean way to estimate any percentile directly from the mean: for an exponential distribution, P(T > t) = e^(−t/mean), so the 99th percentile sits at mean × ln(100) ≈ mean × 4.6. Applying that to both utilization levels:
| Utilization ρ | Throughput | Mean latency (W) | p99 latency (≈ W × 4.6) | vs. 70% throughput |
|---|---|---|---|---|
| 50% | 100 req/s | 10.0 ms | 46.1 ms | −29% |
| 70% | 140 req/s | 16.7 ms | 76.8 ms | baseline |
| 80% | 160 req/s | 25.0 ms | 115.1 ms | +14% |
| 90% | 180 req/s | 50.0 ms | 230.3 ms | +29% |
| 95% | 190 req/s | 100.0 ms | 460.5 ms | +36% |
| 99% | 198 req/s | 500.0 ms | 2,302 ms (2.3 s) | +41% |
Read the two ends of that table together. Going from 70% to 99% utilization buys 41% more throughput — and costs a 30× increase in p99 latency (76.8 ms to 2.3 seconds). A capacity plan that reasons in terms of "we still have 20 points of headroom before 100%" is reasoning about the wrong axis entirely; by the time average utilization reads 90%, the p99 request is already three times slower than it was at 70%, and the plan that "looks 10 points from full" at 90% is, in delay terms, already most of the way up a wall.
This specific mean-to-p99 multiplier (≈4.6×) is exact only under M/M/1's assumptions — Poisson arrivals and exponential service times, both of which have a coefficient of variation of exactly 1. Real production traffic is very rarely that well-behaved, and — as the next section covers — it's essentially always worse than this model, not better. Treat the 4.6× figure as the optimistic floor for how bad your own tail is relative to your mean, not as a number to copy directly into a capacity plan.
Why an average-utilization dashboard misses the cliff
☺ Like you're 10: Two separate reasons a "we're averaging 70%, we're fine" dashboard can be dead wrong at the exact same moment a pager goes off for breached tail latency.
The worked example above already shows the curve is steep. This section explains why the steepness is systematically invisible to the metric most capacity plans actually watch — average utilization, sampled over some monitoring interval. Two independent mechanisms combine to hide it.
Mechanism one: the averaging window smooths away the moments that matter
A Prometheus rate over a 1- or 5-minute window, or a CloudWatch 5-minute datapoint, reports the average utilization across that whole interval. But (1 − ρ) punishes instantaneous utilization, not windowed-average utilization — and a window that averages out to a calm 70% can easily contain ten or twenty seconds where instantaneous ρ was 95% or higher: a garbage-collection pause, a retry storm from a downstream dependency, a noisy neighbor on shared infrastructure, a cron job kicking off a batch scan. Because the delay formula is convex — it curves upward, not linearly — those brief high-ρ seconds contribute far more than their share of queueing delay to whichever requests happen to arrive during them, and that's exactly what surfaces as p99 or p999 latency. The average-utilization line on the dashboard, and often even the average-latency line, barely moves, because the many calm seconds in the window dilute the few bad ones back down to a reassuring-looking mean. The tail metric doesn't get diluted the same way, because it's specifically designed to report the worst moments, not average them away.
Mechanism two: real traffic is burstier than the model assumes, which makes it worse, not better
The M/M/1 model in the previous section assumes Poisson arrivals and exponential service times — both "memoryless," both with a coefficient of variation of exactly 1. Real systems deviate from that in a specific, well-documented direction. A landmark 1994 measurement study by Leland, Taqqu, Willinger, and Wilson, "On the Self-Similarity of Ethernet Traffic," found that real network traffic is statistically self-similar — bursty at every timescale examined, not smoothing out into a tidy average the way a Poisson process would — and three decades of production telemetry since has kept finding the same shape in application-layer request traffic: retries cluster, batch jobs and cron schedules synchronize, client libraries reconnect in thundering herds after a blip.
The generalization of the M/M/1 result that accounts for this is Kingman's formula (J. F. C. Kingman, "The Single Server Queue in Heavy Traffic," 1961), often called the VUT equation because it factors queueing delay into Variability, Utilization, and (service) Time:
Wq ≈ [(Ca² + Cs²) / 2] × [ρ / (1 − ρ)] × Ts
Ca² = squared coefficient of variation of interarrival times
(variance of gaps between arrivals, divided by mean² )
Cs² = squared coefficient of variation of service times
Ts = mean service time
ρ/(1 − ρ) is the same Utilization term from the M/M/1 result
For M/M/1 specifically: Ca² = 1, Cs² = 1 → (Ca²+Cs²)/2 = 1
→ Kingman's formula reduces exactly to the M/M/1 result above.
(a useful consistency check — M/M/1 is Kingman's formula at
its calmest possible setting, not a different formula)The practical reading: the Variability term (Ca² + Cs²)/2 is a multiplier on top of the same utilization curve. When arrivals or service times are burstier than Poisson/exponential — retry storms, GC pauses, cache-miss stampedes, N+1 query blowups, cold starts — that term rises above 1, and real queueing delay at a given average ρ is worse than the already-alarming M/M/1 numbers in the previous section's table, not better. The reverse is also true and often overlooked as a lever: making arrivals or service times more uniform — jittering client-side retries instead of letting them synchronize, coalescing duplicate in-flight requests, timing out fast instead of long-tailing, evening out payload sizes — lowers the Variability term and buys real queueing-delay headroom at the exact same average utilization. None of that shows up on a CPU-percentage dashboard, which is precisely why it's a lever most capacity plans never pull.
"We're bursty, so we run at lower average utilization to be safe" is the right instinct applied to the wrong variable. Lower average ρ helps, but a service with high Ca²/Cs² can still get punished harder at 60% utilization than a perfectly uniform service at 80%, because the Variability multiplier applies at every point on the curve, not just near saturation. If your traffic is bursty by nature — webhooks, cron-triggered batch consumers, anything downstream of a retry policy you don't control — treat the M/M/1 numbers in this page as a floor for your actual risk, and prioritize smoothing burstiness (queues with backpressure, jitter, coalescing) alongside, not instead of, raising the utilization target.
Beyond one server: pooling and Erlang C
☺ Like you're 10: A shared line of customers waiting for the next free cashier moves faster, on average, than the same number of customers each stuck waiting for one specific cashier — and that's the whole reason a shared server pool beats siloed capacity at the same total size.
Production capacity is almost never a single M/M/1 queue — it's a fleet of interchangeable instances behind a load balancer, or a shared pool of worker threads, database connections, or goroutines, all drawing from one arrival stream. The relevant Kendall model is M/M/c: Poisson arrivals, exponential service, c identical servers sharing one queue. The formula for the probability an arriving request has to wait at all — the Erlang C formula — is considerably messier than the M/M/1 results above, but its history and its practical implication are both worth knowing.
A hundred-year-old formula, unchanged
Agner Krarup Erlang, an engineer at the Copenhagen Telephone Company, derived the formulas for how many trunk lines a switchboard needed to handle a given call volume with an acceptable wait probability, publishing the foundational work in 1917. The "erlang" — a dimensionless unit of traffic intensity, exactly analogous to ρ × c in this page's notation — is named for him, and the Erlang C formula he derived for waiting systems still sizes call-center staffing and telephony trunk groups today, essentially unchanged. The formula generalized from switchboards to web fleets and connection pools without needing to be reinvented, because the underlying mathematics — arrivals competing for a fixed number of interchangeable servers — is identical.
Why pooling helps: the practical takeaway
The result that matters operationally, without working through the Erlang C algebra directly: c servers sharing one queue can safely sustain a higher average ρ than c separate, siloed single-server queues carrying the same total load, because pooling averages variance across more servers — a burst that would saturate one isolated queue gets absorbed by whichever of the c servers happens to be free at that moment. This is precisely why a shared connection pool or shared worker pool consistently outperforms per-caller dedicated pools at the same total capacity, and why an evenly load-balanced fleet outperforms one where traffic is unevenly pinned across shards or instances — even when both report an identical fleet-wide average utilization. Uneven distribution is a hidden tax: the overloaded shard is deep in its own local knee of the curve while the underloaded one sits comfortably idle, and the average across both looks fine right up until the overloaded shard's tail latency shows up in your global p99.
Pooling only delivers this benefit if the pool is genuinely fungible — any server can serve any request. Sticky sessions, single-threaded per-tenant queues, and shard-pinned routing all defeat pooling entirely: each pinned slice behaves like its own isolated M/M/1 queue, hitting its own local knee at its own local ρ, regardless of how healthy the fleet-wide average looks. Before trusting a fleet's average utilization as a safety signal, confirm the load balancer is actually spreading load evenly across it — see database reliability engineering for exactly this failure mode in connection-pool and shard design, and multi-region & multi-AZ architecture for its cross-region form.
Turning the math into a capacity-planning policy
☺ Like you're 10: Five concrete habits an SRE builds from everything above, so the next "it was fine until it wasn't" gets caught in a load test instead of a page at 2 a.m.
Everything above compresses into a short list of practices that separate a capacity plan from a capacity guess.
1. Choose a target utilization deliberately, not a round number
"We aim for 80% CPU" is a policy that made sense for a perfectly uniform, Poisson-ish workload and makes considerably less sense for a bursty one — the Variability term in Kingman's formula means the safe target ρ genuinely differs by workload. A latency-sensitive service with retry storms and uneven payloads should target meaningfully lower (often 50–65%); a uniform, latency-tolerant batch workload can safely run closer to the knee. Pick the number from the curve and your own measured Ca²/Cs², not from a habit — and revisit it whenever the SLO in SLIs, SLOs & error budgets changes, since a tighter latency SLO mechanically demands more headroom under this same math.
2. Alert and autoscale on a leading indicator, not CPU%
In-flight request count — Little's Law's L — is a direct, immediate proxy for how full the queue actually is. CPU utilization is an indirect, lagging one, and for I/O-bound services it can look entirely calm while a request queue is already backing up behind a saturated downstream dependency. Watching L (queue depth, active connections, pending goroutines) directly, and scaling or shedding load on it, catches the front edge of the knee; watching CPU alone catches it only after the knee has already been fully climbed. This is also the more direct metric to feed into monitoring & observability dashboards and burn-rate alerts.
3. Use Little's Law as an admission-control formula, directly
The reversed form from earlier in this page — max_concurrency ≈ target_throughput × target_latency — is a usable limit, not just a sizing estimate. Reject or shed load once in-flight requests hit that ceiling, rather than letting the queue grow unbounded and dragging every in-flight request's latency down together:
function admit(request):
if in_flight >= max_concurrency: # from Little's Law:
reject(request, 503) # target_throughput × target_latency
return # fail fast, protect what's already in flight
in_flight += 1
try:
handle(request)
finally:
in_flight -= 1This is the mechanism behind adaptive concurrency limiters in production use today — Netflix has written publicly about its open-source concurrency-limits library doing exactly this, and both Envoy and gRPC ship adaptive concurrency filters built on the same idea; check each project's current documentation before adopting one, since the exact algorithms (fixed limits vs. gradient-based adaptive ones) evolve. The pattern connects directly to the load-shedding and circuit-breaker material in reliability patterns — this page is the arithmetic that justifies where to set the limit those patterns enforce.
4. Load-test past the knee before production finds it for you
Every number in this page's worked example was computable in advance, which means the knee is discoverable in a controlled load test rather than a live incident. Tools built for exactly this — k6, Locust, and Gatling — exist specifically to push a service past its comfortable 70–80% zone and record where p99 actually bends, and that measurement belongs as a standard checklist item in a production readiness review, not an optional nice-to-have.
5. Budget headroom the same way you budget availability
If capacity headroom is what keeps a service off the vertical part of the curve, then burning through it deserves the same seriousness as burning an availability error budget — a p99-latency SLI degrading toward its threshold is a leading indicator of the exact failure this page describes, and it's worth alerting on with the same rigor. Practice both the arithmetic and the response with Drill — Forecast the Bottleneck and the hands-on Capstone Part 5 — Capacity Plan & Load Test.
Every practice above is the same instruction applied five different ways: stop treating "percent utilization" as the number that matters, and start treating "distance from the knee of 1/(1−ρ), given your actual burstiness" as the number that matters. The first is a diagnostic that lags an incident. The second is a forecast that prevents one.
Ellie the Elephant: Dashboard says we're averaging 70% CPU across the fleet for the last hour. Looks perfectly healthy to me.
Pip the Hummingbird: Then why did I just get paged at 2 a.m. for a p99 burn-rate alert on the exact same service?
Foxy: That's the part I don't get. How can the fleet be "fine" and paging someone at the same time? Pick one.
Sol the Sloth: ...I already worked it out while you were all talking. Your five-minute average smooths right over it. For maybe forty seconds in that window, instantaneous utilization was past ninety percent — a retry storm, probably. 1 over 1 minus 0.9 is ten. 1 over 1 minus 0.7 is three point three. Everyone in the queue during those forty seconds paid the ten, not the seventy percent everyone else saw.
Benny the Beaver: My autoscaler only triggers off average CPU, on a five-minute window. By the time it added capacity, the queue had already backed up and drained again. I built the wrong trigger.
Timmy the Turtle: Then trigger on queue depth, not CPU — that's the number Little's Law actually cares about. And stop calling 70% average "safe" until you know how bursty this traffic really is underneath it.
Professor Owl: Which is the whole lesson, stated plainly: the average was never lying. It just wasn't answering the question anyone was actually asking.
1. State Little's Law precisely, and explain what makes it hold regardless of arrival distribution, service-time distribution, or scheduling discipline. 2. In the M/M/1 formulas, what single term is common to L, Lq, W, and Wq, and why does it make the utilization-vs-delay relationship explosive rather than linear as ρ → 1? 3. In the worked example (5 ms baseline service time, μ = 200 req/s), what is mean latency at ρ = 0.7 and at ρ = 0.9, and roughly how many times worse is it? 4. Name the two separate mechanisms that let an average-utilization dashboard look healthy while p99 latency has already breached an SLO. 5. What does Kingman's formula add on top of the basic M/M/1 result, and what does it mean for a service whose traffic is burstier than Poisson? 6. Why can a pool of c servers sharing one queue safely run at a higher average utilization than c siloed single-server queues carrying the same total load — and what defeats that benefit?
Check your answers
- Little's Law: L = λ·W — the average number of items in a stable system equals the average arrival rate times the average time each item spends in the system. It holds for any stable queueing system (arrivals eventually equal departures) with no assumption about the arrival process, service-time distribution, or scheduling discipline — it's a conservation law, not a model-specific result.
- The common term is
(1 − ρ), appearing in the denominator of every M/M/1 formula. As ρ approaches 1,(1 − ρ)approaches 0, and since it's a denominator, every formula's output grows without bound — not linearly, but hyperbolically — which is why delay stays nearly flat at low utilization and turns almost vertical near saturation. - At ρ = 0.7: W = 5/0.3 = 16.7 ms. At ρ = 0.9: W = 5/0.1 = 50.0 ms — exactly 3× worse, for only a 28.6% increase in traffic (140 → 180 req/s).
- (a) Averaging windows (e.g. a 5-minute utilization metric) smooth away short bursts of high instantaneous utilization, so a window that reports 70% can contain seconds of 95%+ ρ that inflict disproportionate delay on unlucky requests without moving the average. (b) Real traffic is burstier than the Poisson/exponential assumptions behind the basic M/M/1 numbers, so actual delay at a given average ρ is typically worse than those numbers suggest, not better.
- Kingman's formula (the VUT equation) multiplies the M/M/1-style utilization term by a Variability factor,
(Ca² + Cs²)/2, built from the coefficients of variation of interarrival and service times; for Poisson/exponential traffic this factor is exactly 1 and the formula reduces to the M/M/1 result. For burstier-than-Poisson traffic (retry storms, GC pauses, uneven payloads), the factor rises above 1, meaning real queueing delay at a given average utilization is worse than the M/M/1 model predicts. - Pooling lets any of the
cservers absorb a burst that would otherwise saturate one isolated queue, averaging variance across more capacity — the M/M/c / Erlang C effect. It's defeated by anything that pins requests to one specific server instead of the shared pool — sticky sessions, per-tenant single-threaded queues, or uneven shard/region routing — each of which then behaves like its own isolated M/M/1 queue, hitting its own local knee regardless of a healthy fleet-wide average.