--- title: "How to Tune PHP-FPM Pool Settings on Ubuntu 24.04" description: "Size PHP-FPM pm.max_children from real memory use, pick dynamic, static or ondemand mode, enable the status page and slow log, and verify it on Ubuntu 24.04." category: "Performance Optimization" language: en source: https://cubepath.com/docs/performance-optimization/php-fpm-optimization-pool-configuration updated: 2026-09-25 --- # How to Tune PHP-FPM Pool Settings on Ubuntu 24.04 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: ```bash grep -Ev '^\s*(;|$)' /etc/php/8.3/fpm/pool.d/www.conf ``` ```output [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: ```bash systemctl is-active php8.3-fpm ps -C php-fpm8.3 -o pid,user,rss,cmd ``` ```output 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: ```bash 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}' ``` ```output 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: ```bash ps --no-headers -C php-fpm8.3 -o rss,cmd --sort=-rss | awk '/pool/ {printf "largest: %.0f MB\n", $1/1024; exit}' ``` ```output 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: ```text 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: ```bash free -m ``` ```output 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: | Item | Memory | |---|---| | Total RAM | 4096 MB | | Operating system and Nginx | -512 MB | | MySQL (buffer pool and connections) | -1024 MB | | Safety margin | -512 MB | | **Available for PHP-FPM** | **2048 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: | Mode | Behaviour | Best for | |---|---|---| | `dynamic` | Keeps between `pm.min_spare_servers` and `pm.max_spare_servers` idle workers, up to `pm.max_children` | Most servers with steady traffic (the default) | | `static` | Always runs exactly `pm.max_children` workers | Dedicated, high-traffic servers where memory is reserved for PHP anyway | | `ondemand` | Starts workers only when requests arrive and stops them after `pm.process_idle_timeout` | Small 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: ```bash 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: ```ini 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: ```ini 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: ```bash sudo php-fpm8.3 -t ``` ```output [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: ```bash 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: ```bash sudo nano /etc/php/8.3/fpm/pool.d/www.conf ``` Find and uncomment (or add) these directives: ```ini 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: ```bash 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: ```bash sudo apt install libfcgi-bin ``` ```bash sudo env SCRIPT_NAME=/fpm-status SCRIPT_FILENAME=/fpm-status REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock ``` ```output 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: ```nginx 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: ```bash 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: ```bash 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: ```bash 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: ```bash sudo grep "max_children" /var/log/php8.3-fpm.log ``` ```output [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`: ```bash sudo adduser --system --group --no-create-home site1 ``` Create the pool file: ```bash sudo nano /etc/php/8.3/fpm/pool.d/site1.conf ``` ```ini [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: ```bash sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm ls -l /run/php/ ``` ```output -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: ```nginx 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.