PHP-FPM runs PHP code in a pool of worker processes that sit behind a web server such as Nginx. Its default pool on Ubuntu allows only five workers, which is safe on the smallest server but makes busy sites queue requests, time out and return 502 or 504 errors. In this tutorial you will measure how much memory your PHP workers really use, calculate a safe pm.max_children, choose a process manager mode, enable the status page and slow log, and check that the pool is sized correctly under load on Ubuntu 24.04.

Prerequisites

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • Nginx and PHP-FPM 8.3 serving a PHP application (a LEMP stack). If you only have PHP-FPM, install it with sudo apt install php8.3-fpm.

All paths in this guide are for PHP 8.3, the version shipped with Ubuntu 24.04. If you use a different version, replace 8.3 in paths and service names.

Step 1 - Reviewing the default pool

The main PHP-FPM file is /etc/php/8.3/fpm/php-fpm.conf, and each pool is defined in its own file under /etc/php/8.3/fpm/pool.d/. Ubuntu creates a single pool named www. Show its active (non-comment) settings:

grep -Ev '^\s*(;|$)' /etc/php/8.3/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

These settings mean:

  • listen: PHP-FPM accepts requests on a Unix socket, which Nginx reaches with fastcgi_pass unix:/run/php/php8.3-fpm.sock;.
  • pm = dynamic: the number of workers changes with load, between the spare server limits.
  • pm.max_children = 5: at most five requests are processed at the same time. The sixth waits in the socket queue.

Confirm that the service is running and see the current worker processes:

systemctl is-active php8.3-fpm
ps -C php-fpm8.3 -o pid,user,rss,cmd
active
    PID USER       RSS CMD
    812 root     22148 php-fpm: master process (/etc/php/8.3/fpm/php-fpm.conf)
    845 www-data 13272 php-fpm: pool www
    846 www-data 13272 php-fpm: pool www

The master process runs as root and only manages the workers; the pool www processes run as www-data and execute your code.

Step 2 - Measuring the memory used per worker

pm.max_children must be set so that all workers together fit in RAM. If they do not, the server starts swapping or the kernel kills processes, which is much worse than queueing requests. You therefore need the real memory footprint of a worker running your application, not a guess.

Workers grow as they execute code, so measure them after the site has served real traffic for a while (or after you have browsed through its heaviest pages). Then calculate the average resident memory (RSS) of the pool workers:

ps --no-headers -C php-fpm8.3 -o rss,cmd | awk '/pool/ {sum+=$1; n++} END {if (n) printf "workers: %d  average: %.0f MB\n", n, sum/n/1024}'
workers: 3  average: 58 MB

Also look at the largest worker, because a few heavy requests (imports, reports, image processing) can use much more memory than the average:

ps --no-headers -C php-fpm8.3 -o rss,cmd --sort=-rss | awk '/pool/ {printf "largest: %.0f MB\n", $1/1024; exit}'
largest: 96 MB

RSS includes memory shared between workers (such as OPcache), so these numbers overestimate slightly. That gives you a built-in safety margin.

Typical values are 30 to 60 MB for simple PHP sites and 60 to 120 MB for WordPress with many plugins, Magento or large Laravel applications.

Step 3 - Calculating pm.max_children

Use this formula:

pm.max_children = RAM available for PHP / memory per worker

To find the RAM available for PHP, start from the total memory and subtract everything else that runs on the server: the operating system (about 300 to 500 MB), the database, Redis or Memcached, and a safety margin. Check total and used memory with:

free -m
               total        used        free      shared  buff/cache   available
Mem:            3915         912         410          45        2848        3003
Swap:              0           0           0

A worked example for a 4 GB server that also runs MySQL:

ItemMemory
Total RAM4096 MB
Operating system and Nginx-512 MB
MySQL (buffer pool and connections)-1024 MB
Safety margin-512 MB
Available for PHP-FPM2048 MB

With an average worker of 60 MB rounded up to 70 MB for headroom, 2048 / 70 is about 29, so pm.max_children = 28 is a safe value. If some requests regularly reach the size of the largest worker, use that number instead of the average.

More workers than your CPUs can serve does not increase throughput for CPU-bound code. PHP applications usually spend much of their time waiting for the database or external APIs, so values of several workers per core are normal, but if CPU usage is already at 100 percent with the current workers, adding more only increases response times.

Step 4 - Choosing a process manager mode

PHP-FPM has three process manager modes:

ModeBehaviourBest for
dynamicKeeps between pm.min_spare_servers and pm.max_spare_servers idle workers, up to pm.max_childrenMost servers with steady traffic (the default)
staticAlways runs exactly pm.max_children workersDedicated, high-traffic servers where memory is reserved for PHP anyway
ondemandStarts workers only when requests arrive and stops them after pm.process_idle_timeoutSmall servers, many low-traffic pools, development machines

dynamic is a good default. Use ondemand when memory is tight or you host many small sites, each in its own pool. Use static only when the server is dedicated to PHP and you have already calculated pm.max_children carefully, because all workers use memory all the time.

Step 5 - Applying the pool configuration

Open the pool file:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Find the process manager directives (they are spread through the file with long comments) and set them to values that match your calculation. This example is for the 4 GB server from Step 3:

pm = dynamic
pm.max_children = 28
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500

A few guidelines for the other values:

  • pm.start_servers: workers created at startup. It must be between pm.min_spare_servers and pm.max_spare_servers; about 20 percent of pm.max_children works well.
  • pm.min_spare_servers / pm.max_spare_servers: how many idle workers PHP-FPM keeps ready for traffic spikes. Idle workers still use memory, so keep the maximum moderate.
  • pm.max_requests: each worker is restarted after this many requests. It limits the impact of memory leaks in extensions or application code. Values between 500 and 1000 are common; 0 disables it.

If you choose ondemand, the spare server settings are ignored; set these instead:

pm = ondemand
pm.max_children = 28
pm.process_idle_timeout = 10s
pm.max_requests = 500

Test the configuration before applying it. PHP-FPM refuses to start with an invalid pool file, so always run this check first:

sudo php-fpm8.3 -t
[25-Sep-2026 12:10:31] NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful

Reload the service. A reload replaces the workers gracefully without dropping requests in progress:

sudo systemctl reload php8.3-fpm

Step 6 - Enabling the status page and slow log

The status page reports how busy the pool is, and the slow log records a stack trace for requests that take too long. Both are essential to confirm your sizing. Open the pool file again:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Find and uncomment (or add) these directives:

pm.status_path = /fpm-status
ping.path = /fpm-ping

slowlog = /var/log/php8.3-fpm-slow.log
request_slowlog_timeout = 5s
request_terminate_timeout = 60s
  • request_slowlog_timeout: any request running longer than 5 seconds gets its PHP stack trace written to the slow log, which shows exactly which function it was stuck in.
  • request_terminate_timeout: kills a worker whose request runs longer than 60 seconds, so a hung request cannot occupy a worker forever. Set it above your longest legitimate request, and keep it consistent with fastcgi_read_timeout in Nginx.

Test and reload:

sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm

You can query the status page directly through the FastCGI socket, without exposing it in Nginx, using the cgi-fcgi tool:

sudo apt install libfcgi-bin
sudo env SCRIPT_NAME=/fpm-status SCRIPT_FILENAME=/fpm-status REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Content-type: text/plain;charset=UTF-8

pool:                 www
process manager:      dynamic
start time:           25/Sep/2026:12:14:02 +0000
start since:          842
accepted conn:        12877
listen queue:         0
max listen queue:     0
listen queue len:     511
idle processes:       7
active processes:     3
total processes:      10
max active processes: 14
max children reached: 0
slow requests:        0

The values that tell you whether the pool is sized correctly are:

  • max children reached: how many times all workers were busy since the last start. Anything above zero means requests had to wait.
  • listen queue and max listen queue: requests waiting for a free worker right now and at the worst moment. These should stay at or near zero.
  • max active processes: the highest number of busy workers so far. If it is always far below pm.max_children, you can lower the limit and free memory.
  • slow requests: requests that exceeded request_slowlog_timeout.

If you prefer to check the status from a browser or a monitoring system, add a location to your Nginx server block that only allows localhost:

location ~ ^/(fpm-status|fpm-ping)$ {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Step 7 - Verifying under load

Generate some load against a PHP page to see how the pool behaves. ab from apache2-utils is enough for a quick check. Replace your_domain and use a real page of your application:

sudo apt install apache2-utils
ab -n 2000 -c 40 https://your_domain/

While the test runs, watch the status page in a second terminal:

watch -n 1 "sudo env SCRIPT_NAME=/fpm-status SCRIPT_FILENAME=/fpm-status REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock | grep -E 'active processes|listen queue:|max children'"

And check memory at the same time:

free -m

A well-sized pool shows these signs under your normal peak load:

  • listen queue stays at 0 or briefly above it, and max children reached does not grow.
  • available memory in free -m stays comfortably above zero and swap is not used.
  • ab reports Failed requests: 0.

If the queue grows while memory is still free, raise pm.max_children. If memory runs low before the queue grows, lower it or reduce the memory per worker. Run load tests against a staging copy, or during quiet hours, because they compete with real visitors.

PHP-FPM also warns in its log when the pool is too small:

sudo grep "max_children" /var/log/php8.3-fpm.log
[25-Sep-2026 12:31:07] WARNING: [pool www] server reached pm.max_children setting (28), consider raising it

Step 8 - Using separate pools per site (optional)

When one server hosts several applications, give each its own pool. Each pool runs as its own Linux user, has its own worker limit and its own socket, so one busy or compromised site cannot take all the workers or read another site's files.

Create a system user for the site, replacing site1:

sudo adduser --system --group --no-create-home site1

Create the pool file:

sudo nano /etc/php/8.3/fpm/pool.d/site1.conf
[site1]
user = site1
group = site1

listen = /run/php/php8.3-fpm-site1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 10s
pm.max_requests = 500

pm.status_path = /fpm-status
slowlog = /var/log/php8.3-fpm-site1-slow.log
request_slowlog_timeout = 5s

php_admin_value[open_basedir] = /var/www/site1:/tmp
php_admin_value[memory_limit] = 256M

listen.owner stays www-data so that Nginx can connect to the socket, while PHP code runs as site1. Make sure the site's files are readable by that user, test and reload:

sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm
ls -l /run/php/
-rw-r--r-- 1 root     root      4 Sep 25 12:40 php8.3-fpm.pid
srw-rw---- 1 www-data www-data  0 Sep 25 12:40 php8.3-fpm-site1.sock
srw-rw---- 1 www-data www-data  0 Sep 25 12:40 php8.3-fpm.sock
lrwxrwxrwx 1 root     root     30 Sep 25 12:40 php-fpm.sock -> /etc/alternatives/php-fpm.sock

In the Nginx server block of that site, point fastcgi_pass to the new socket:

fastcgi_pass unix:/run/php/php8.3-fpm-site1.sock;

Remember that the pm.max_children values of all pools add up; the memory calculation from Step 3 applies to their total.

Troubleshooting

Nginx returns 502 Bad Gateway. Check /var/log/nginx/error.log. connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory) means PHP-FPM is not running or the socket path is wrong; start it and read sudo journalctl -u php8.3-fpm. (13: Permission denied) means Nginx cannot access the socket; check listen.owner and listen.group.

Nginx returns 504 Gateway Timeout. A request took longer than Nginx waits (fastcgi_read_timeout, 60 seconds by default) or every worker was busy. Check the slow log for the slow code path and the status page for max children reached.

The server swaps or processes are killed by the OOM killer. pm.max_children is too high for the memory available. Recalculate it with the largest worker size, lower it, and check sudo dmesg | grep -i "out of memory" to confirm.

PHP-FPM fails to start after an edit. Run sudo php-fpm8.3 -t; it prints the file and line that contain the error.

Conclusion

You measured the real memory footprint of your PHP workers, calculated a safe pm.max_children, chose a process manager mode, enabled the status page and slow log, and verified the pool under load. From now on, the status page and the max_children warnings in the log tell you when the pool needs to grow.

As next steps, tune OPcache so that each request does less work, move sessions or application cache to Redis to reduce database load, and add the PHP-FPM status metrics to your monitoring so you see trends before visitors see errors.