How to Configure PHP-FPM Pools for Performance

Size pm.max_children from real memory numbers, choose dynamic or ondemand, set timeouts and the slow log, run one pool per site, and stop 502 and 504 errors.

Published
Reading time
4 min

PHP-FPM is the process manager that runs PHP behind Nginx or Apache. Its defaults are written for a machine with unknown RAM and unknown traffic, which means they are wrong for yours: either too few workers (requests queue, 502/504 errors under load) or too many (the server runs out of memory and swaps). Sizing it takes five minutes and a calculator.

Where the config is

Each pool is a file: /etc/php/8.3/fpm/pool.d/www.conf on Ubuntu/Debian, /etc/php-fpm.d/www.conf on EL. On Plesk, each domain has its own pool under /var/www/vhosts/system/<domain>/etc/php-fpm.conf, and the settings below are exposed under PHP Settings → Performance settings for the domain.

The one number that matters: pm.max_children

The maximum number of PHP processes that can run at once. Too low and requests wait; too high and memory runs out. The calculation:

  1. Find out how much memory one PHP process uses on your site under load:

    bash
    ps -ylC php-fpm8.3 --sort:rss | awk '{sum+=$8; n++} END {print sum/n/1024 " MB average, " n " processes"}'

    WordPress typically lands at 40–80 MB; Laravel 30–60 MB; a big WooCommerce site with many plugins 100+ MB.

  2. Decide how much RAM PHP may have: total minus what the OS, MariaDB, Nginx and Redis need. On a 4 GB server with MariaDB taking 1 GB, that is about 2 GB for PHP.

  3. Divide: 2048 MB / 60 MB ≈ 34. Round down.

ini
pm.max_children = 32

That is the ceiling. Starting from a wrong ceiling is how servers with 2 GB of RAM end up with pm.max_children = 200 and a kernel OOM kill of MariaDB at the first traffic spike.

dynamic or ondemand

ini
pm = dynamic
pm.max_children = 32
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12

dynamic keeps a few workers warm and scales up to the max — the right choice for a site with steady traffic. ondemand starts workers only when requests arrive and kills them after pm.process_idle_timeout; it uses no memory when idle, at the cost of a few milliseconds' startup per burst. Use ondemand on a server hosting many small sites, each with its own pool, so idle sites cost nothing:

ini
pm = ondemand
pm.max_children = 16
pm.process_idle_timeout = 10s

static (a fixed number always running) is for a dedicated server running one busy application where you want zero scaling latency.

Recycle workers

PHP applications leak a little memory over thousands of requests. Restart each worker periodically:

ini
pm.max_requests = 500

Timeouts, and the 504

Nginx gives up waiting for PHP after fastcgi_read_timeout (default 60 s) and returns a 504. PHP gives up on itself after max_execution_time (default 30 s). For a normal site both are fine; for a report or an import that takes minutes, raise them for that location only rather than globally, or move the work to a queue.

ini
request_terminate_timeout = 120s

in the pool kills a request that has run too long, which stops one stuck script eating a worker forever.

Find the slow scripts

ini
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s

Every request over five seconds writes a stack trace to the slow log — exactly which plugin, which function, which query. This is the fastest way to answer "why is the site slow" and it costs nothing.

One pool per site

On a server with several sites, give each its own pool with its own user, group and socket. Sites cannot then read each other's files, a runaway on one site cannot starve the others, and each can have its own PHP version and limits. This is how Plesk does it by default and worth copying by hand:

ini
[example.com]
user = example
group = example
listen = /run/php/example.com.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 12

and in the Nginx server block, fastcgi_pass unix:/run/php/example.com.sock;.

Apply and verify

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

Then watch under load:

bash
watch -n 2 "ps -C php-fpm8.3 --no-headers | wc -l; free -m | grep Mem"

Worker count should rise with traffic and fall after; memory should never approach zero available. If journalctl -u php8.3-fpm shows "server reached pm.max_children", you are hitting the ceiling — either raise it (if memory allows), add caching so fewer requests reach PHP, or add RAM.

On a VPSPioneer managed VPS, pool sizing is part of setup: we measure the real per-process footprint of your application and set the limits from it rather than from a default file. Where the memory goes on the database side is in MySQL and MariaDB tuning basics.

#php#php-fpm#nginx#performance#servers

Keep reading

More from Servers

All guides

Servers

Apache vs Nginx: Which Web Server Should You Run?

How Apache and Nginx differ in architecture, performance and .htaccess support, why many hosts run both, and which to choose for PHP, an API or static files.

3 min read →

Servers

How to Install Nginx, PHP-FPM and MariaDB (LEMP) on Ubuntu

A complete LEMP setup on Ubuntu 24.04 — Nginx, PHP 8.3-FPM, MariaDB — with a working server block, a secure database, sensible PHP settings and HTTPS.

3 min read →

Servers

Managed vs Unmanaged VPS: Which One Do You Actually Need?

What a host does on a managed VPS and what is left to you on an unmanaged one, the real time cost of self-management, and a short test for which to choose.

3 min read →