Reading the machine
Every value carries how it was obtained: read, inferred, or defaulted. A confidently stated worker count that was really a guess is the single most common way a capacity prediction goes wrong.
The worker pool
The most important number Heat has, and the hardest to obtain. PHP inside FPM has no API for asking the pool how large it is — the status page would say, but it is off on most installs and unreachable on the rest — so Heat reads the pool configuration directly, across the layouts the distributions settled on:
/usr/local/etc/php-fpm.d/*.conf/etc/php/*/fpm/pool.d/*.conf/etc/php-fpm.d/*.conf/opt/homebrew/etc/php/*/php-fpm.d/*.conf/opt/bitnami/php/etc/php-fpm.d/*.conf- Apache’s
MaxRequestWorkers
It prefers the pool belonging to the PHP that is running, and explicitly skips one belonging to a different version. A box with several PHP versions installed — every Debian machine that has ever been upgraded, and every container image that ships the range — will otherwise hand you the worker count of an interpreter that has not executed anything in years. Wrong in a way that looks entirely plausible.
When nothing is readable, Heat estimates from RAM รท memory_limit at 70% — roughly the calculation a sysadmin does by hand, and roughly as reliable — and marks it inferred.
Cores and memory: cgroup first
On anything containerised the host’s figures are a lie. /proc/cpuinfo faithfully reports the 32 cores of the machine your container is scheduled on, while the cgroup quota says you may use two. Size a worker pool off the first number and the container gets throttled into the ground with every dashboard showing idle CPU — one of the more baffling ways for a site to be slow.
So Heat reads cpu.max (cgroup v2) and cpu.cfs_quota_us (v1) before it consults the host, and the same for memory limits. Host figures are the fallback, not the answer.
The database
Two numbers, and the entire point is that they are different numbers.
max_connections is how many clients may be connected. It is usually in the hundreds, it is what everybody quotes, and it is not a throughput limit. How many queries the server can genuinely run at the same time is a function of its cores, and it is usually a small number.
Confusing the two is why “we raised max_connections and it got slower” is a thing people say — four hundred connections on four cores does not run four hundred queries, it runs four and queues three hundred and ninety-six, and the queueing costs more than the refusal would have. Heat reports both, models both, and tells them apart.
Query parallelism is inferred rather than read: no database server exposes it. On a shared host it is the host’s core count; off-host, Heat says it assumed and asks to be told.
Craft’s own settings
The configuration that changes capacity before any hardware does.
| Finding | Why it matters |
|---|---|
devMode on |
Logs every query, recompiles Twig, renders a debug toolbar. The same code can be several times slower. No capacity planning is worth doing until it is off. |
runQueueAutomatically on |
The queue is not a background process at all — it is HTTP requests to your own site, taking workers from the same pool as your visitors. A resave of fifty thousand entries competes directly with traffic. |
| File cache | Per-machine. Works, and stops working the moment there is a second web server — so the horizontal scaling that was going to solve the capacity problem quietly introduces a consistency one. |
enableTemplateCaching off |
Every {% cache %} tag on the site is doing nothing, and whatever capacity they were buying has already been given back. |
| Transforms generated on demand | The first visitor to an untransformed image pays for it while holding a worker — and on a launch, that lands on every image at once. |
| No static cache in front | The only change that multiplies capacity rather than adding to it. |
Overriding it
Heat probes whatever it is running on, and what it is running on is very often a laptop. Being able to say “model production, which has 64 workers and 16 cores” without deploying anything is not a convenience — it is most of the value.
php craft heat/capacity --workers=64 --cores=16 --memory=32768 --connections=500
Or set them permanently in Settings. Zero means “probe for it”; anything else wins, and is recorded as a stated fact rather than an inference — a human who knows their production configuration is a better source than any probe.
To see what Heat could read here and what it had to guess:
php craft heat/capacity/machine
Degrading honestly
Shared hosts disable shell_exec; containers have no /proc/meminfo; macOS has no /proc at all; open_basedir hides half the filesystem. Every probe is written for the host that says no: nothing throws, absence is reported as absence rather than filled in with a plausible number, and the report names the fields Heat had to infer.