How the model works
Two layers, and the split is the whole design: the ceiling is arithmetic, the latency is a model, and Heat never presents them in the same voice.
Service centres
Every finite resource a request must get through is described the only two ways that matter: how many of it there are (m), and how long one request occupies one of them (D).
| Centre | Servers m | Demand D |
|---|---|---|
| PHP workers | pm.max_children, capped by RAM | the whole request |
| CPU | cores, or the cgroup quota | measured CPU time |
| Database | real query parallelism | measured database time |
| Database connections | max_connections less reserved | the whole request |
A worker pool and a connection limit are different things physically and identical arithmetically, which is exactly why Heat can say which one you run out of first rather than just that you got slow.
The ceiling
Little’s law with the queue removed: a centre cannot complete more than m/D requests per second, and the system cannot exceed the smallest of those.
That number is a bound, not an estimate. It holds regardless of arrival pattern, scheduling, or how the load is shaped. Everything else Heat reports is a model; this is arithmetic.
The queue
How bad it gets before the ceiling comes from an M/M/c queue solved through Erlang-C. Two implementation notes that matter:
Erlang-B, not the textbook formula
The usual Erlang-C expression needs ac/c!, and a pool of 200 workers means computing 200! and a200 — both infinite in a double, long before the division cancels them. Every value Heat is asked about sits in exactly the range where that breaks. So both formulas are computed through the Erlang-B recurrence instead, which is stable for any c because every step is a ratio of quantities of the same magnitude.
The worst single queue, not the sum
Adding the waits from every centre would count the same delay several times: a request is not queueing for CPU and a worker and a connection as three stacked delays — it is stuck behind whichever one it cannot get, while the others sit idle underneath it.
Heat takes the maximum rather than assuming the binding centre wins, because at moderate load a centre with a small ceiling but very few servers can queue harder than one with a smaller ceiling and hundreds: with c servers the wait falls off roughly as 1/(c − a), so eight workers at 70% queue noticeably while a hundred and fifty connections at 75% barely queue at all.
A closed population
N visitors, each of whom pauses for Z seconds and then makes one request. Throughput is found by solving
f(x) = x − N / (Z + S + W(x))
for its root — the rate at which “how often people click” and “how long they wait” agree. The function is strictly increasing, negative at zero and positive at the ceiling, so it has exactly one root and bisection cannot miss it.
Response time then comes from Little’s law: N people each spending Z thinking and R waiting complete N/(Z + R) requests per second, so R = N/X − Z. Deriving R from a throughput Heat already believes is what stops a projection row ever contradicting itself — which a model computing the two separately would eventually do, at exactly the load level someone was about to make a decision at.
Percentiles
Response time is two things added together with very different shapes. The service part is roughly lognormal and fairly tight; its median sits a little under the mean and its 95th a little over. The waiting part is a queue, close to exponential, whose median is 0.693 of its mean and whose 95th is three times it.
Splitting them and recombining is why Heat’s p95 grows sharply as the queue appears rather than tracking the mean at a fixed multiple — which is what a single blanket factor would do, and what would make the interesting part of the curve the wrong shape.
Verdicts
| Verdict | Means |
|---|---|
| Comfortable | Under 70% of the binding resource, and latency near its unloaded value |
| Queueing | Still serving every request, but requests are waiting and latency has grown |
| Falling over | Past 95% utilisation, or p95 past the request timeout, or more requests waiting than the listen backlog holds |
Deliberately three states rather than a percentage. “You are at 84% of capacity” is a number nobody acts on; “this is where requests start queueing” is a decision.
Failure is reported as the thing that actually happens: past the listen backlog it is a 502, past the request timeout it is a 504.
The ladder always shows the interesting part
The default ladder is a set of round numbers — plus the exact load at which the site stops being comfortable, a rung inside the queueing band, and the exact load at which it falls over. Without those, a ladder can step from 250 visitors (fine) to 500 (in ruins) and hide the entire region anybody was actually asking about.
What Heat charges to what
- Database CPU counts against the web tier only when the database shares the machine. With a warm buffer pool most query time is CPU rather than disk, so 60% of it is charged to the same cores — a judgement, stated as one number in one place so a reader who disagrees can see exactly what it changes.
- Memory is not a queue, so it is not modelled as one. It caps the worker count instead.
- A proxy-cache hit costs nothing on the origin but still counts toward the response time a visitor experiences. A visitor who waited 8ms waited 8ms.