Queues and tails: why 19 % more load makes latency four times worse
Utilisation went from 0.80 to 0.95 — a rise of nineteen percent. Time in the system rose fourfold. The lesson takes that disproportion apart and, along the way, refutes the received wisdom that "the tail grows faster than the mean": in a pure queue it does not, and what pulls the tail away from the middle is something else.
Full technical treatment
TL;DR
The latency of a request is service time plus waiting time, and what grows with load is the waiting. While the server keeps up, gaps appear between requests and the queue drains in them. The closer the arrival rate gets to what the server can handle, the rarer those gaps become — and the longer every random burst of requests stands around. The work itself does not get a millisecond more expensive.
Hence the main consequence: latency grows out of proportion to load. In the model, utilisation rose 1.19x (from 0.80 to 0.95) while the mean time in the system rose 4.1x, from 51.3 ms to 212.9, with service unchanged at 10 ms. So "we still have fifteen percent of headroom" is not fifteen percent of latency: running at 0.95 costs four times the latency of 0.80 on the same hardware with the same code. The same goes for charts: the mean and the median move together with the tail, so "average latency is fine" does not yet prove that p99 is.
Beyond that are the numbers, the boundaries and one refutation. There is one model here and it is named outright: M/M/1 — a single server, an unbounded queue, exponential inter-arrival times and exponential service with a 10 ms mean. In that model, raising utilisation from 0.80 to 0.95 grew p99 by 4.1x — exactly as the mean did — and the p99-to-mean ratio stayed put: 4.43 against 4.38. What follows is not the opposite law but the absence of one: "the tail always grows faster than the mean simply because of the queue" is a bad generalisation, because in M/M/1 it grows at the same rate. With a heavy-tailed service time, several servers, batching or priorities the picture differs — and this lesson measures one such case: at the same utilisation of 0.80 and the same 10 ms mean service time, replacing even service with "5 ms in 95 % of cases and 105 ms in 5 %" raised p99 3.1x and the median 2.6x. Which is also why the reverse picture on a chart — "the tail jumped and the mean barely moved" — points at spread rather than at load. And one more boundary: the ρ of the model is not the CPU utilisation on your dashboard.
- a server does not handle requests instantly: each one takes some amount of time;
- requests arrive whenever they arrive rather than on a schedule, so sometimes they arrive in bursts;
- a request that reaches a busy server waits its turn.
- percentiles, and how p99 differs from the mean and the median;
- utilisation ρ, the
M/M/1model, the exponential distribution, "the spread of service times".
What is actually being asked
The ladder usually runs like this:
- "What is p99?" — the warm-up.
- "Why can you not look at the mean?" — where the substance starts.
- "Load grew by a fifth and latency went up fourfold. Why?" — the central question.
- "How highly can you load a service?" — about headroom.
- "Why is the tail longer than the middle?" — usually answered with "because of load", which is wrong.
- "What do you do about the tail?" — about what can actually be changed.
The numbers in this lesson come from a model rather than from a run on a live service. The reason is the same as in the lesson on retries: observing how latency depends on utilisation means changing the load on a working service. The model gives the same dependency with named assumptions; in exchange the reader must remember that a real service has several workers, a bounded queue and timeouts.
Base: a queue appears when requests arrive faster than they are served
Before talking about utilisation, percentiles and models it is worth naming, in ordinary words, where a queue comes from at all.
Suppose a server can handle ten requests per second. That is its limit — it cannot do more.
- Eight arrive per second. The server keeps up. Sometimes requests arrive in a burst and somebody has to wait, but a gap follows the burst and the queue drains inside it. There is headroom, and the headroom is those gaps.
- Ten arrive per second. The server is busy the whole time. There are no gaps any more — and with no gaps there is nothing for the queue to drain into: whatever piled up during a burst stays there and keeps growing.
That is the whole reason a queue behaves so strangely near the limit: what disappears is not capacity but the gaps.
The second thing to name before anything else is what the latency a user sees is made of:
service time + waiting time = latency
Service time is how long the server spends on the request itself. Waiting time is how long the request stood in the queue while the server was busy with others. The user sees only the sum, but the two terms change differently — and that is what this lesson is about.
And here is the central question: which of the two terms grows when things "got slower"? The work has not changed: the same code, the same hardware, the same request.
The answer: the waiting grows. Service stays exactly what it was, while time in the queue keeps increasing — because the gaps that drain it keep getting rarer.
That is already enough to answer the basic interview question. Everything below is about how disproportionately the waiting grows, where the gap between the middle and the tail comes from, and why what moves that gap is not what people usually assume.
Mechanism 1: a queue gets expensive faster than load grows
TIME IN THE SYSTEM AGAINST UTILISATION
--------------------------------------
load mean p50 p95 p99
0.50 20.3 14.0 61.2 94.0
0.70 34.1 23.6 103.2 158.1
0.80 51.3 35.8 155.7 227.2
0.90 109.2 74.1 331.9 503.2
0.95 212.9 152.9 634.0 932.5
The "load" column is the fraction from the Base: eight requests out of ten the server can take is utilisation 0.8, nine and a half is 0.95. Formally, the utilisation ρ of the model is the share of time the single server is busy.
And straight away, the boundary without which that number misleads: the ρ of the model is not the CPU utilisation on your dashboard. CPU says how hard the hardware is working; ρ says how densely occupied the one place where the queue forms is. They need not agree: the bottleneck may be a disk, a database connection, a worker pool or a lock — and then the queue grows by every rule in this table while the CPU chart stays calm. Read the table with the ρ of the resource that actually constrains you, not of the most visible one.
Read the "mean" column: 20 → 34 → 51 → 109 → 213. Load grows in equal steps; time accelerates.
The reason is not that the service got tired. Service takes the same 10 ms throughout; only waiting grows. The mechanism: while the worker is busy, new requests pile up, and the higher the utilisation the rarer are the gaps in which a queue drains. At 0.5 there are many gaps, at 0.95 almost none, and each random burst takes longer and longer to clear.
Hence the rule worth answering with: the last percentage points of utilisation are the most expensive. In the "mean" column the step from 0.5 to 0.8 is 20.3 → 51.3, and the next step, from 0.8 to 0.95, is 51.3 → 212.9 — fourfold.
Mechanism 2: putting a number on the disproportion
WHAT GROWS FASTER THAN WHAT
---------------------------
load 0.80 -> 0.95 is a rise of 1.19x
mean time rises by 4.15x
p99 rises by 4.10x
p99 over mean at load 0.80 4.43x
p99 over mean at load 0.95 4.38x
The first two lines answer the central question: load rose by 19 %, latency fourfold. That is the disproportion that makes plans of the form "we still have fifteen percent of headroom" wrong: fifteen percent of utilisation is not fifteen percent of latency.
The third and fourth lines are why the model is worth reading closely. The ratio of p99 to the mean did not change: 4.43 against 4.38. In M/M/1 — the model this whole table comes from — p99 and the mean scale identically: the tail grows at exactly the rate the middle does, not faster.
Why that happens is visible in the model's own construction, and it is worth naming: time in the system here is exponentially distributed, and for an exponential distribution any quantile is the mean times a constant factor. That is why the ratio of p99 to the mean cannot change — what grows is the factor in front of it, not the ratio.
Now, carefully, about what follows from that. It does not follow that the tail never grows faster than the mean. Exactly one thing follows: "the tail always grows faster than the mean simply because of the queue" is not a general law — there exists a queue where it is untrue, and it happens to be the most thoroughly studied queue in existence. The rule is written from the boundary: in M/M/1 the tail and the mean scale together; outside it, that is an open question.
And a real system leaves that boundary in many ways. A heavy-tailed service time (one class of requests that takes an order of magnitude longer than the rest), several servers instead of one, batching (requests processed in groups), priorities (some requests let through first) — in each of those the picture differs, and the tail may well part company with the middle. The first of them is measured in this lesson, and that is where we go next.
Mechanism 3: the tail is made by spread, not by load
The hypothesis: what pulls the tail away from the middle is the spread of service times, not the load. The test — the same load, the same average service time, only the spread changes: instead of "10 ms on average", "95 % of requests take 5 ms and 5 % take 105 ms". The mean of that is exactly the same: 0.95 × 5 + 0.05 × 105 = 10.
SAME MEAN SERVICE TIME, DIFFERENT SPREAD
----------------------------------------
both runs: load 0.80, average service time 10 ms
A: service time exponential, average 10 ms
B: service time is 5 ms in 95% of cases and 105 ms in 5%
mean p50 p99
A, exponential 51.3 35.8 227.2
B, mixed 139.1 92.2 713.8
p99 of B over p99 of A 3.14x
p50 of B over p50 of A 2.58x
Everything grew — the median and the tail alike — but the tail grew further: 3.1x against 2.6x. And utilisation did not move by a single percent, nor did the average work per request.
This is exactly the step outside M/M/1 mentioned above: the moment service time acquired a heavy tail, the tail of latency moved faster than the middle. Note the order of the claims — not "the tail grows faster than the mean because queues", but "the tail grows faster than the mean when the spread changes; load alone is not enough to do it".
Hence the phrasing to answer with: the tail is made by spread, not by load. Five percent of slow requests is not "five percent of users will wait"; it is a queue forming behind them, and everybody else joins it.
And the practical consequence: to improve p99 you look for the slow class of requests rather than adding capacity in general. Capacity lowers utilisation and moves the whole distribution; removing spread moves the tail specifically.
Deeper: what the model leaves out
The model behind this lesson is M/M/1, and its name is worth carrying alongside its numbers: almost every conclusion holds inside its boundaries rather than in general. Four things worth saying yourself.
There is one worker. A real service handles several requests in parallel, which changes the numbers a lot: a queue in front of several workers drains faster, and the same utilisation costs less. The direction of the dependency survives; the exact multiples do not. This is also where "p99 and the mean grow together" breaks: that equality was shown for M/M/1 and nowhere else.
The queue is unbounded. Nobody is refused and nobody leaves. A real system
has an accept queue of finite length — the one described in tcp(7) and taken
apart in the lesson on the accept queue — and client
timeouts (the lesson on timeouts): both cut the tail off — at the price of errors
instead of waiting.
There are no retries. In life there are, and they add load exactly when the system is at its limit (the lesson on retries). The real curve is steeper than the model's by that contribution.
And ρ once more. Utilisation in the model is the share of time the one place where the queue forms is busy. A dashboard usually has no such number: it has CPU utilisation, while the queue may be sitting at a connection pool, a disk or a lock. Applying this table to CPU percentages is the most common mistake made when carrying these conclusions into a live system.
How to answer in an interview
Short answer: time in the system is service plus waiting, and what grows with utilisation is the waiting, not the service. So the dependency is disproportionate: in the model, raising utilisation from 0.80 to 0.95 is plus 19 %, and time grew 4.1x with the work unchanged at 10 ms per request.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
Three things separate a good answer. First, you do not pass "the tail grows faster than the mean" off as a law of queues: in the M/M/1 model the p99-to-mean ratio is constant (4.43 against 4.38), so load alone stretches the whole distribution as one. Second, you name the real cause of a long tail — the spread of service times; in the same model, at the same load and the same mean, swapping even service for "95 % at 5 ms and 5 % at 105 ms" tripled p99 and raised the median a little over twofold. Third, you draw the practical conclusion: p99 is fixed by finding the slow class of requests, not by adding capacity in general.
And two things that are easy to overdo. First: "p99 and the mean grow together" is a property of M/M/1, not a law of queues. With a heavy-tailed service time, several servers, batching or priorities the tail does part company with the middle — the spread measured in this lesson is exactly such a case. Second: the ρ of the model is not the CPU utilisation on a dashboard; before applying the table to your system, work out which resource the queue is actually forming at.
Next they ask
How highly can a service be loaded?
The answer follows from the table rather than from habit: in the "mean" column 0.5 → 0.8 is 20.3 → 51.3, and 0.8 → 0.95 is 51.3 → 212.9 — fourfold. So "how much" is a question about the latency you are willing to pay and about how sharp your spikes are.
Headroom exists for the spike, not for the average: a system sitting at 0.95 falls into a bottomless queue on a short peak, while one at 0.7 rides it out and returns.
Why does adding a second worker help more than making the service 20 % faster?
Because the two act on different parts of the sum. Speeding up reduces service time — which at high utilisation is already a small part of the time in the system. A second worker reduces utilisation and therefore the waiting, and it is the waiting that grows disproportionately.
The model shows it indirectly: at utilisation 0.95 the mean time is 212.9 ms with 10 ms of service, so 95 % of the time the request is waiting. Optimising the five percent is a poor investment.
How do you shorten the tail without touching capacity?
Reduce the spread. In the model that was the only thing that moved the tail more than the middle: the same utilisation, the same mean service time — and p99 three times higher because of five percent of slow requests.
In practice that means finding the slow class (heavy requests, a cold cache, misses that reach the database) and either speeding it up or moving it onto a separate path so that it does not delay the rest. The second is often cheaper and always more predictable.
Is p99 "the worst 1 % of users"?
Not quite, and the correction matters: p99 is computed over requests, not people. A user whose page makes a hundred requests is almost certain to land in that percent at least once — so what they see as slow is not 1 % of cases but a noticeable share of their pages.
Hence the practice of watching not only request p99 but the share of sessions with at least one slow request. It usually turns out an order of magnitude larger than the percentage suggests.
Common misconceptions
latency grows in proportion to load
Model: a 1.19x rise in utilisation (0.80 to 0.95) produces a 4.1x rise in time in the system. Only waiting grows — service takes the same 10 ms throughout. So "we still have fifteen percent of headroom" is not fifteen percent of latency.
as load grows the tail grows faster than the mean — that is a law of queues
It is not a law. In the M/M/1 model, from 0.80 to 0.95 both the mean and p99 grew 4.1x, and the p99-to-mean ratio stayed put (4.43 against 4.38): load alone stretches the whole distribution as one. Outside that model — a heavy-tailed service time, several servers, batching, priorities — the tail really can grow faster, and this lesson measures that on spread.
a long tail is a sign of overload
Not necessarily: the tail is made by spread. In the model, at an unchanged utilisation of 0.80 and an unchanged mean service time of 10 ms, replacing even service with "95 % at 5 ms and 5 % at 105 ms" raised p99 3.1x. Utilisation did not move by a percent.
if the mean is fine, the tail is fine
In M/M/1 the mean and the tail move together, so "the mean is fine" proves nothing — it simply has not grown yet. The case of "the tail grew and the mean barely did" does occur, and it means one specific thing: the spread grew, that is, a slow class of requests appeared.
to improve p99 you make the processing faster
At high utilisation service is a small part of the time: in the model at 0.95 the mean time in the system is 212.9 ms with 10 ms of service, so 95 % of the time the request is waiting. Speeding up a five-percent share changes almost nothing; reducing utilisation and spread changes everything.
p99 is the worst percent of users
It is the worst percent of requests. A user whose page makes a hundred requests will almost certainly land in that percent at least once, so the share of affected sessions is an order of magnitude above the share of slow requests. Both are worth counting.
a single-server queue model has nothing to do with a real service
Its numbers do not: a real service has several workers, a bounded queue, timeouts and retries, and every multiple will differ. Its direction does: waiting grows faster than load, and spread pulls the tail. That is exactly why M/M/1's assumptions are named one by one and its conclusions are stated as ratios rather than milliseconds — and why its ρ is kept separate from the CPU utilisation on a dashboard.
Practice
Two exercises. Answer first, then check against the real output: in both, the correct answer comes from a run of the model rather than being written by hand.
Practice · predict the output
low = simulate(0.80)
high = simulate(0.95)
mixed = simulate(0.80, service_mixed)
print(f"{high['mean'] / low['mean']:.1f}")
print(f"{high['p99'] / low['p99']:.1f}")
print(f"{mixed['p99'] / low['p99']:.1f}")Practice · estimate
Knowledge check
Utilisation rises from 0.80 to 0.95. What happens to the mean time in the system?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- The latency of a request is service time plus waiting time, and what grows with load is the waiting. While the server keeps up, gaps appear between requests and the queue drains in them. The closer the arrival rate gets to what the server can handle, the rarer those gaps become — and the longer every random burst of requests stands around. The work itself does not get a millisecond more expensive.
- Hence the main consequence: latency grows out of proportion to load. In the model, utilisation rose 1.19x (from 0.80 to 0.95) while the mean time in the system rose 4.1x, from 51.3 ms to 212.9, with service unchanged at 10 ms. So "we still have fifteen percent of headroom" is not fifteen percent of latency: running at 0.95 costs four times the latency of 0.80 on the same hardware with the same code. The same goes for charts: the mean and the median move together with the tail, so "average latency is fine" does not yet prove that p99 is.
- Beyond that are the numbers, the boundaries and one refutation. There is one model here and it is named outright: M/M/1 — a single server, an unbounded queue, exponential inter-arrival times and exponential service with a 10 ms mean. In that model, raising utilisation from 0.80 to 0.95 grew p99 by 4.1x — exactly as the mean did — and the p99-to-mean ratio stayed put: 4.43 against 4.38. What follows is not the opposite law but the absence of one: "the tail always grows faster than the mean simply because of the queue" is a bad generalisation, because in M/M/1 it grows at the same rate. With a heavy-tailed service time, several servers, batching or priorities the picture differs — and this lesson measures one such case: at the same utilisation of 0.80 and the same 10 ms mean service time, replacing even service with "5 ms in 95 % of cases and 105 ms in 5 %" raised p99 3.1x and the median 2.6x. Which is also why the reverse picture on a chart — "the tail jumped and the mean barely moved" — points at spread rather than at load. And one more boundary: the ρ of the model is not the CPU utilisation on your dashboard.
In fact
- Model: a 1.19x rise in utilisation (0.80 to 0.95) produces a 4.1x rise in time in the system. Only waiting grows — service takes the same 10 ms throughout. So "we still have fifteen percent of headroom" is not fifteen percent of latency.
- It is not a law. In the M/M/1 model, from 0.80 to 0.95 both the mean and p99 grew 4.1x, and the p99-to-mean ratio stayed put (4.43 against 4.38): load alone stretches the whole distribution as one. Outside that model — a heavy-tailed service time, several servers, batching, priorities — the tail really can grow faster, and this lesson measures that on spread.
- Not necessarily: the tail is made by spread. In the model, at an unchanged utilisation of 0.80 and an unchanged mean service time of 10 ms, replacing even service with "95 % at 5 ms and 5 % at 105 ms" raised p99 3.1x. Utilisation did not move by a percent.
- In M/M/1 the mean and the tail move together, so "the mean is fine" proves nothing — it simply has not grown yet. The case of "the tail grew and the mean barely did" does occur, and it means one specific thing: the spread grew, that is, a slow class of requests appeared.
- At high utilisation service is a small part of the time: in the model at 0.95 the mean time in the system is 212.9 ms with 10 ms of service, so 95 % of the time the request is waiting. Speeding up a five-percent share changes almost nothing; reducing utilisation and spread changes everything.
- It is the worst percent of requests. A user whose page makes a hundred requests will almost certainly land in that percent at least once, so the share of affected sessions is an order of magnitude above the share of slow requests. Both are worth counting.
- Its numbers do not: a real service has several workers, a bounded queue, timeouts and retries, and every multiple will differ. Its direction does: waiting grows faster than load, and spread pulls the tail. That is exactly why M/M/1's assumptions are named one by one and its conclusions are stated as ratios rather than milliseconds — and why its ρ is kept separate from the CPU utilisation on a dashboard.
What is covered
- What is actually being asked
- Base: a queue appears when requests arrive faster than they are served
- Mechanism 1: a queue gets expensive faster than load grows
- Mechanism 2: putting a number on the disproportion
- Mechanism 3: the tail is made by spread, not by load
- Deeper: what the model leaves out
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
1 SOURCE
- tcp(7), Linux man-pages 6.7Official documentation. The one place where this lesson leans on a document rather than a model: the queue of established connections in which a request waits its turn is the one described in listen(2)/tcp(7) and taken apart in the lesson on the accept queue. Everything else here is a simulation with stated assumptions (
bench/queueing/mm1.py), not a measurement of a live system.https://man7.org/linux/man-pages/man7/tcp.7.html