PHP-FPM Tuning Basics: Process Manager, OPcache and Slow Logs
Learn how PHP-FPM pools work, how to size pm.max_children from real memory use, and which OPcache settings help most on a busy PHP site or WordPress server.
PHP-FPM (FastCGI Process Manager) is how most modern servers run PHP behind Nginx or Apache. It keeps a pool of PHP worker processes ready to handle requests. Out of the box the defaults are conservative and work for small sites, but on a busy WordPress or Laravel server a poorly sized pool is a common cause of slow pages, 502 errors and out-of-memory kills. This guide explains the few settings that matter and how to choose values from measurement, not guesswork.
Where the settings live
On Debian and Ubuntu with PHP 8.3, pools are defined in /etc/php/8.3/fpm/pool.d/, with www.conf as the default pool. Substitute your own PHP version number. On Red Hat style systems the location varies with how PHP was installed. After editing, test and reload:
sudo php-fpm8.3 -t
sudo systemctl reload php8.3-fpm
The process manager modes
The pm directive controls how workers are created:
- static keeps a fixed number of workers. Predictable and fast, but they use memory even when idle.
- dynamic keeps a range of workers, starting some and adjusting between minimum and maximum spare counts. It is the usual default and a good balance.
- ondemand starts workers only when requests arrive and stops them when idle. It saves memory on servers hosting many rarely visited sites, at the cost of a small delay when a worker has to start.
Sizing pm.max_children from real numbers
The most important setting is pm.max_children, the hard limit on simultaneous PHP workers. Too low, and requests queue up; too high, and the server runs out of memory and starts swapping or killing processes. Find the average memory of one worker while the site is under typical load:
ps -ylC php-fpm8.3 --sort:rss
Look at the RSS column (in kilobytes). Suppose workers average about 60 MB (this is only an illustration; your applications will differ). If you decide that 2 GB of RAM can go to PHP after leaving room for the database, web server and operating system, then 2048 / 60 gives roughly 34, so you would set pm.max_children a little below that. The method matters, not the number:
max_children = memory you can spare for PHP / average memory per worker.
Then set the others in proportion for dynamic mode:
pm = dynamic
pm.max_children = 30
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 500
The pm.max_requests setting recycles a worker after that many requests, which limits the effect of memory leaks in extensions or plugins. If PHP-FPM logs the warning "server reached pm.max_children setting", requests were waiting for a free worker, which is your cue to examine slow code or raise the limit if memory allows.
OPcache: the biggest easy win
PHP normally compiles your scripts on every request. OPcache stores the compiled code in shared memory so that step is skipped. It ships with PHP and is usually enabled, but check the values, typically in /etc/php/8.3/fpm/conf.d/10-opcache.ini:
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
Large applications with many files, such as WordPress with many plugins, may need a larger memory_consumption and a higher max_accelerated_files. If you deploy code with a controlled process, you can set validate_timestamps=0 for extra speed, but then you must reload PHP-FPM after every deployment or your changes will not appear.
Find slow code with the slow log
Add these lines to the pool to record a backtrace whenever a request runs longer than a threshold:
request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-slow.log
The log shows which function was running, which is often enough to point to a slow plugin, query or external API call. Also consider request_terminate_timeout to stop runaway requests, and keep memory_limit in php.ini realistic, because each worker can grow up to that limit.
Sockets, separate pools and measured tuning
Sockets and Nginx
Pools listen either on a Unix socket or on a local TCP port. Sockets avoid a little network overhead and are the Debian and Ubuntu default. With Nginx, the matching PHP block looks like this (the snippet file ships with the Debian and Ubuntu packages; other systems need the fastcgi_param lines written out):
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Separate pools
Create one pool per site, each with its own system user and socket. That isolates sites from each other so a compromise or a runaway script in one does not directly give access to the files of another. Control panels such as Plesk create per-site pools for you.
Tuning is measurement
Change one setting at a time, watch memory with free -h or your monitoring tool, and check the slow log and error log afterwards. Note that no PHP setting can rescue a database that is missing indexes or a page that makes fifty uncached queries; fix the application first when profiling points there.
Check the result
After each change, test under realistic load rather than guessing. A simple tool such as ab or wrk can request a page repeatedly while you watch top and the PHP-FPM status. Enabling the status page (pm.status_path = /fpm-status, exposed only to localhost) shows active and idle workers and the listen queue length. A growing queue means requests are waiting for a free worker. If your site uses a CDN, see our CDN page, since serving cached pages at the edge reduces the load PHP-FPM ever sees.
Fast PHP hosting at Zebra Hosting
On our Plesk-based WordPress hosting the PHP setup is managed for you. If you want full control of pools and OPcache for a larger project, a VPS lets you tune them yourself.