Queueing Theory & Little's Law

The minimal queueing math for capacity reasoning — Little's law, the M/M/1 utilization hockey stick, and Kingman's variability factor — plus the sizing rules that fall out and the places the models stop applying.

Every service you run is a queueing system: requests arrive at some rate, wait somewhere (an accept queue, a thread pool, a connection pool, a run queue), get served, and leave. Three pieces of math — Little's law, the M/M/1 response-time curve, and Kingman's approximation — cover most day-to-day capacity reasoning, and they explain concretely why "CPU is only at 85%" and "p99 is on fire" happen at the same time.

Little's law #

For any system in steady state, the mean number of items inside it equals arrival rate times mean time spent inside:

L=λWL = \lambda W

What makes it universally applicable: it assumes nothing about arrival distributions, service time distributions, scheduling order, or how many servers there are. The only requirement is stationarity — over the measurement window, what flows in flows out. You can therefore draw the boundary anywhere — one mutex, one thread pool, one service, a whole pipeline — and the law holds for each boundary separately.

The workhorse form for sizing: concurrency = throughput × latency. A service doing 2,000 req/s at 50 ms mean has 2000×0.05=1002000 \times 0.05 = 100 requests in flight. If its worker pool has 64 threads, 36 requests are always waiting somewhere else — and their queueing time is part of that 50 ms. The law also inverts: given a fixed concurrency limit LL (a pool of 100 connections) and downstream latency W=10W = 10 ms, the ceiling on throughput is L/W=10,000L/W = 10{,}000 req/s.

Utilization and the hockey stick #

Let λ\lambda be the arrival rate and μ\mu the service rate (one server completes μ\mu requests/s, so mean service time τ=1/μ\tau = 1/\mu). Utilization is ρ=λ/μ\rho = \lambda/\mu. For the simplest model — M/M/1, Poisson arrivals and exponential service times, one server — mean response time and mean queue wait are:

W=1μ−λ=τ1−ρ,Wq=ρμ−λ=ρ τ1−ρW = \frac{1}{\mu - \lambda} = \frac{\tau}{1-\rho}, \qquad W_q = \frac{\rho}{\mu - \lambda} = \frac{\rho\,\tau}{1-\rho}

The denominator μ−λ\mu - \lambda is the spare capacity available to drain the queue. As ρ→1\rho \to 1 it goes to zero, so response time doesn't degrade linearly — it explodes:

ρ = 1 0 0.5 0.8 0.9 1 1× 5× 10× 20× utilization ρ = λ/μ mean response time W (× τ) W = τ/(1−ρ) the knee: past ≈0.8 every point of ρ costs multiples of τ
M/M/1 mean response time in multiples of service time τ. The curve is nearly flat below ρ ≈ 0.7, then goes vertical: the same traffic increment that was free at ρ = 0.5 is catastrophic at ρ = 0.9.
W = τ/(1−ρ) ρ > 0.8 — wait dominated by queueing

The same numbers as a table — bars are linear, 2 px per multiple of τ\tau:

Utilization ρMean response time W
0.502 τ
0.703.3 τ
0.805 τ
0.9010 τ
0.9520 τ
0.99100 τ

Going from 50% to 80% utilization costs 2.5× latency. Going from 90% to 99% costs another 10×. And these are means — the tail percentiles sit further out on the same explosion (see tail latency & jitter sources).

Variability multiplies the pain: Kingman #

M/M/1 fixes both distributions. Kingman's approximation (G/G/1 — arbitrary arrivals and service times, one server) separates the wait into three independent factors:

Wq  ≈  ρ1−ρ⏟utilization⋅ca2+cs22⏟variability⋅  τW_q \;\approx\; \underbrace{\frac{\rho}{1-\rho}}_{\text{utilization}} \cdot \underbrace{\frac{c_a^2 + c_s^2}{2}}_{\text{variability}} \cdot\; \tau

where cac_a and csc_s are the coefficients of variation (stddev / mean) of interarrival and service times. For exponential distributions ca2=cs2=1c_a^2 = c_s^2 = 1, the variability factor is 1, and the formula collapses to the exact M/M/1 result. For perfectly regular arrivals and constant service times it's 0 — a metronome feeding a constant-time server never queues.

Real systems sit above 1, often far above:

  • Bursty arrivals (ca2>1c_a^2 > 1): cron-aligned batch jobs, clients that retry in sync, mobile fleets waking on a push notification. Same average λ\lambda, several times the wait.
  • Heavy-tailed service times (cs2>1c_s^2 > 1): one endpoint that does 200 ms of work sharing a pool with 2 ms lookups, GC pauses, cold caches. A single slow request holds the server while the queue behind it grows.

The factors multiply. A service at ρ=0.8\rho = 0.8 with ca2=cs2=4c_a^2 = c_s^2 = 4 (entirely realistic for mixed workloads) queues 4×4\times longer than M/M/1 at the same load: Wq=16τW_q = 16\tau instead of 4τ4\tau. (The table above lists response times W=Wq+τW = W_q + \tau — its 5τ5\tau at ρ=0.8\rho = 0.8 is that 4τ4\tau queue wait plus one service time.)

Sizing rules that fall out #

Latency-sensitive services get a utilization ceiling around 0.7–0.8. At ρ=0.9\rho = 0.9 mean response is already 10 service times, and both the burst sensitivity and the tail are worse than the mean. This is the whole story behind "running at 80–90% CPU destroys tail latency": the last 20% of the machine is only cheap for throughput, never for latency. Batch pipelines that care about throughput alone can run at 0.95+; anything with a p99 SLO cannot.

Multiple servers soften the knee, but don't remove it. With kk workers sharing one queue, a burst only queues when all kk are busy, so bigger pools tolerate higher ρ\rho at the same wait. The classic rule of thumb (square-root staffing, from the Erlang-C model) is to provision k≈R+cRk \approx R + c\sqrt{R} servers for offered load R=λτR = \lambda\tau — headroom grows with the square root of load, so big services genuinely can run hotter than small ones.

Pools are the LL in Little's law. A connection pool or thread pool caps concurrency at its size; required concurrency is λW\lambda W. The failure mode: a downstream slowdown raises WW, so at unchanged λ\lambda the needed concurrency rises past the pool size, the pool saturates, and the queue reappears upstream of it — latency propagates backwards through the system. This is why saturated pools need explicit backpressure or load shedding rather than unbounded internal queues, and why autoscaling should trigger on utilization, which moves early and smoothly, not on latency, which moves late and vertically.

Where the models break #

Open vs closed systems. The formulas above describe open systems: arrivals don't care how the system is doing. A closed-loop load generator (k virtual users, each sending the next request after the previous response) is a different animal — when the target slows, its own arrival rate drops with it, so it can never push ρ→1\rho \to 1. Schroeder et al. showed the two setups produce wildly different response-time curves for the same service. Production internet traffic is mostly open; if your benchmark is closed, it is structurally blind to the hockey stick.

Coordinated omission. The closed-generator problem has a measurement twin: a generator that waits for each response before sending the next one silently drops exactly the samples that would have landed during a stall, biasing percentiles down by orders of magnitude. Covered properly in latency percentiles & coordinated omission; both errors come from letting the system under test throttle the arrival process.

Retries make λ\lambda endogenous. The models assume the arrival rate is a given. With timeouts and retries, effective λ\lambda rises precisely when capacity falls — the mechanism behind retry storms and metastable failures. No steady-state formula applies mid-storm; the fix is architectural (retry budgets, shedding), not analytical.

Non-stationary service times. Warmup, cache eviction, GC cycles, and noisy neighbors mean μ\mu itself drifts. The formulas still work per regime, but a single all-day average of τ\tau predicts nothing about the worst hour.

Sources

  1. bookPerformance Modeling and Design of Computer Systems (Harchol-Balter, 2013)
  2. bookSystems Performance, 2nd ed., ch. 2 Methodologies (Gregg, 2020)
  3. paperA Proof for the Queuing Formula: L = λW (Little, 1961)
  4. paperThe single server queue in heavy traffic (Kingman, 1961)
  5. paperOpen Versus Closed: A Cautionary Tale (Schroeder, Wierman, Harchol-Balter, NSDI 2006)
  6. talkHow NOT to Measure Latency (Gil Tene, 2015)

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close