#15241·phpstan

parallel.loadLimit never reads the system load average

Author: SanderMullerCreated Sep 16, 2026Updated Sep 16, 2026

Bug report

parallel.loadLimit never reads the system load average. The config reference describes it as a fraction of the idle cores, but the shipped behaviour is a fraction of the total cores.

The config reference says:

The number of available cores is determined by the system load average — busy cores are subtracted from the total.

For example, if the system has 11 cores and 5 are busy, there are 6 available cores. Setting loadLimit to 0.7 means PHPStan will use 70% of 6 = 4 cores for parallel workers.

Note that 1.0 does not mean "no limit" — it means 100% of the available cores, not the total number of cores on the machine.

Code snippet that reproduces the problem

neon
parameters:
	level: 0
	paths:
		- app
	parallel:
		loadLimit: 1.0

Run phpstan diagnose -c <that file> on a machine that is busy, and read the Usable CPU cores line. PHPStan 2.2.13 from the phar, 14 cores, load average recorded per run:

loadLimit=1.0    load1min=6.19    Detected CPU cores: 14    Usable CPU cores: 14
loadLimit=0.75   load1min=6.19    Detected CPU cores: 14    Usable CPU cores: 10
loadLimit=0.5    load1min=5.77    Detected CPU cores: 14    Usable CPU cores: 7
loadLimit=0.25   load1min=5.77    Detected CPU cores: 14    Usable CPU cores: 3

Expected output

The documented formula subtracts the busy cores first. Asking the library for both, so the comparison is its own arithmetic and not mine:

loadLimit  load    documented   actual
1.0        6.19    7            14
0.75       6.19    5            10
0.5        5.77    4            7
0.25       5.77    2            3

Actual output

The right-hand column above: floor(loadLimit * 14) every time. The result does not change with the load.

Where it comes from

CpuCoreCounter in phpstan/phpstan-src (src/Process/CpuCoreCounter.php:81) calls the library with three arguments:

php
(new FidryCpuCoreCounter())->getAvailableForParallelisation(0, null, $this->loadLimit)

The fourth parameter is ?float $systemLoadAverage = 0., and the library only calls sys_getloadavg() when it receives null there. With the default the load term is zero, so loadLimit * ($availableCores - 0) is a flat percentage of the total.

The library's own result object reports which value it used. Real load 5.77 at the time:

3 arguments, as PHPStan calls it   availableCpus=14   correctedSystemLoadAverage=0.0
4 arguments, passing null          availableCpus=8    correctedSystemLoadAverage=5.767578125

This has been the behaviour since phpstan/phpstan-src@89f1382, released in 2.1.41.

Why it matters

In #14505 someone asked for a way to stop concurrent PHPStan runs saturating their machine, and the answer was to tune loadLimit. Tuning it does cap the worker count, so that user got what they needed. It is not what they were told they were getting. They have a fixed percentage of all cores, not protection that responds to load.

What I am not asking for

The parameter is not broken as a core-count limiter, and I am not proposing to pass null. That would read a 1-minute average against runs that finish in 20 seconds. It also subtracts load from cores one for one, which turns a second concurrent run into a single worker. The worker count is fixed before the first worker spawns, so it never recovers when the first run ends.

Correcting the config reference looks like the right fix, and possibly the parameter name.