Purpose
This note is the math behind “utilization near one is dangerous.” The key ideas are:
- M/M/1 gives the cleanest closed form
- deterministic service is friendlier than variable service
- tails compound under fan-out
M/M/1 in One Page
Assume:
- Poisson arrivals with rate
- exponential service with rate
- one server
- FIFO
Define utilization
with .
The stationary probability of jobs in the system is geometric:
so
Little’s Law then gives
This is the core scheduling curve. The denominator is the whole story.
Why the Curve Hurts
Take ms.
- at , ms
- at , ms
- at , ms
- at , ms
The service time did not change. The queue did.
Utilization prices latency nonlinearly
With ms, moving from 50% to 90% busy quintuples response time, and 90% to 99% multiplies it by ten again — the work never changed, only the queue. Average-utilization dashboards hide this: the same 0.9 average reached smoothly or in bursts produces very different tails.
Deterministic Service Is Kinder
M/M/1 is analytically convenient and operationally pessimistic because exponential service is highly variable. If service time is constant instead, the queue is M/D/1, and the mean waiting time is lower.
The broader lesson comes from the Pollaczek-Khinchine result for M/G/1:
Since appears, variability matters directly. Two servers with the same mean service time can have very different queueing delay if one has a long heavy tail.
That is why systems that mix tiny requests with rare giant ones often need classification, separate pools, or admission control instead of a single queue.
Burstiness
Arrival variability hurts for the same reason. Average utilization can look fine while bursts create local overload. A queue that drains comfortably on average can still accumulate large backlogs during spikes, and those spikes show up first in high percentiles.
This is where “run at 60% average utilization” comes from. The missing 40% is not waste. It is burst budget.
Tail Percentiles Under Fan-Out
Suppose one user request waits on parallel subrequests, and the overall completion time is the max:
If each subrequest finishes by time with probability , then
So the service-level target for each shard must be stricter than the end-to-end target. If one backend has p99 = 100 ms and a request fans out to 100 independent backends, the probability all are below 100 ms is about
So 100 ms is nowhere near the end-to-end p99 anymore.
This is the central tail-at-scale effect.
Tiny Simulation
import random
from collections import deque
def simulate_mm1(arrival_rate, service_rate, steps, dt=1e-4):
q = deque()
t = 0.0
busy_until = 0.0
responses = []
for _ in range(steps):
t += dt
if random.random() < arrival_rate * dt:
q.append(t)
if t >= busy_until and q:
arrival_t = q.popleft()
service_t = random.expovariate(service_rate)
busy_until = t + service_t
responses.append(busy_until - arrival_t)
return responsesThis is crude, but enough to make the blow-up visible as approaches 1.
How Scheduling Changes the Distribution
Queueing formulas are always conditional on a policy and service model. Changing the scheduling policy can reduce mean or tail latency by:
- prioritizing short jobs
- separating heterogeneous job classes
- preempting long work
- rejecting work before queues run away
- duplicating or speculating on stragglers
The queueing model explains why those moves help. The scheduling policy chooses which one to pay for.