PHP-FPM workers never shrink. Here is how to bound them
A worker measured 30 MB when fresh and 97 MB forty hours later, on an idle server. Nothing was leaking — that is simply how PHP returns memory.
Here is a measurement that surprises people. Restart PHP-FPM, note a worker's memory, then come back two days later on a server nobody has touched:
# minutes after a restart
$ ps -o rss= -C php-fpm8.3 | sort -rn | head -3
31204
30880
30448
# forty hours later, same idle server
99328
91024
90112Three times the memory, no traffic to speak of, and no leak in any application code. This is normal, and understanding why it happens is the difference between guessing at pm.max_children and choosing it.
Why the number only goes up
When PHP frees memory it returns it to its own allocator, not to the operating system. The allocator keeps it for the next request. From the kernel's point of view — which is what ps and your monitoring show — the process never gave anything back.
So a worker's resident size is not "how much it is using". It is the high-water mark of everything it has ever done. One unusually heavy request early on, and that worker carries the cost for the rest of its life.
The setting Debian leaves out
pm.max_requests recycles a worker after it has served N requests: the process exits, a fresh one replaces it, and the high-water mark resets. Debian and Ubuntu ship it commented out, which means workers live until the service restarts.
; /etc/php/8.3/fpm/pool.d/www.conf
pm.max_requests = 500That is the whole fix for unbounded growth. Recycling costs a fraction of a second every 500 requests and bounds a number that otherwise only climbs.
Now max_children means something
pm.max_children is not a performance setting. It is a promise about memory: the maximum number of workers multiplied by what a worker weighs is the most PHP can ever take. The stock value of 24, at the 97 MB those workers had reached, is a promise of 2.3 GB — on a box that also has to run a database, a web server and containers.
Size it from the two numbers you can actually measure:
max_children = (RAM you are willing to give PHP) / (peak worker size)
# example: 1.4 GB budget, 97 MB peak
1400 / 97 = 14Idle workers are the other half
pm.max_spare_servers decides how many workers stay alive after a burst of traffic subsides. At the default of 16, a brief spike leaves sixteen fat workers resident for as long as the service runs. Bringing that down to a quarter of max_children reclaims the memory once the burst is over.
Versions nobody is using
One last thing worth checking, especially on panels that install several PHP versions side by side:
$ systemctl list-units 'php*-fpm*' --no-legend | wc -l
6Six pools, each with a master and its idle workers, when perhaps two of them serve an actual site. The fix is not to disable the unused ones — a site pointed at 8.1 next month would break. Switch them to pm = ondemand with a short idle timeout: the master stays, the socket stays, and no worker exists until a request arrives.
On the server these measurements came from, recycling plus honest ceilings plus ondemand for the unused versions took the whole serving stack from 545 MB to 358 MB, with no change in how fast anything responded.
Alien XP
Runs the servers behind AlphaPanel and writes up what breaks.




