I’ve seen powerful servers grind to a halt under modest traffic because of a few lines in a PHP-FPM configuration file. I’ve also seen small servers handle surprisingly heavy load because those same lines were set sensibly.
PHP-FPM, the FastCGI Process Manager, runs the PHP processes that execute WordPress. Every uncached page view, admin action, AJAX request and API call needs a PHP worker. How many workers you allow, how they’re created and how much memory each one uses determine how much traffic your server can handle before visitors start waiting.
Default settings are rarely right for WordPress. They’re either too conservative, leaving resources unused, or too generous, letting PHP consume so much memory that the server starts swapping or the database gets killed. Here’s how I tune them.
How PHP-FPM handles requests
When a request reaches Nginx and needs PHP, Nginx passes it to PHP-FPM. PHP-FPM assigns it to an available worker process. The worker runs WordPress, returns the result and then waits for the next request.
If all workers are busy, new requests queue up. If the queue grows too long, visitors see slow pages, then 502 or 504 errors.
So two things matter:
- Enough workers to handle concurrent requests.
- Not so many workers that they exhaust the server’s memory.
The art of tuning is finding the balance for your server.
The process manager modes
PHP-FPM can manage workers in three ways, set with the pm directive.
static
A fixed number of workers is always running. It’s simple and predictable, and there’s no delay creating workers when traffic arrives. The downside is that memory is reserved even when the site is idle. Static works well on dedicated servers for a single busy site.
dynamic
PHP-FPM keeps a minimum number of idle workers and creates more as traffic grows, up to a maximum. It’s the most common choice and a sensible default for most WordPress servers.
ondemand
Workers are only created when requests arrive and are stopped after a period of inactivity. This saves memory on servers hosting many low-traffic sites, but the first request after an idle period is slightly slower while a worker starts.
On servers hosting many small sites with separate pools, I often use ondemand for quiet sites and dynamic for busier ones.
Calculating pm.max_children
pm.max_children is the most important setting. It caps the number of workers. Too low, and requests queue during busy periods. Too high, and the server runs out of memory.
The calculation is straightforward:
max_children = memory available for PHP ÷ average memory per PHP process
Step 1: Measure memory per process
Don’t guess. Measure real WordPress processes under normal use:
| <code>ps -ylC php-fpm8.3 –sort:rss | awk 'NR>1 {sum+=$8; n++} END {print "Average MB:", sum/n/1024}'</code> |
|---|
Adjust the process name to match your PHP version. Run this while the site is handling real traffic, ideally including admin use. WordPress processes commonly use somewhere between 40 and 120 MB each, but WooCommerce and heavy page builders can push them higher.
Step 2: Work out available memory
Start with total RAM and subtract what other services need: the database, Redis, Nginx, the operating system and a safety margin.
For example, on a 4 GB server:
- Operating system and Nginx: about 300 MB.
- MariaDB: about 1 GB.
- Redis: about 200 MB.
- Safety margin: about 500 MB.
That leaves roughly 2 GB for PHP.
Step 3: Divide
If each PHP process averages 80 MB, then 2,000 ÷ 80 gives about 25 workers. That’s your pm.max_children.
If you host several sites with separate pools, the combined maximum across all pools should fit within the available memory, although not every pool will hit its maximum at the same time.
Setting the dynamic values
For pm = dynamic, you also set:
- pm.start_servers: workers started when PHP-FPM starts.
- pm.min_spare_servers: minimum idle workers kept ready.
- pm.max_spare_servers: maximum idle workers before extras are stopped.
A reasonable starting point for the example above:
pm.max_requests restarts each worker after it has handled a set number of requests. This guards against slow memory leaks in plugins, which are more common than you’d hope.
For ondemand pools:
Watch for the warning signs
PHP-FPM tells you when it’s struggling. Check its log for messages like:
This means requests were queuing. Before raising the limit, check memory. If the server has spare RAM, raise max_children. If it doesn’t, the real fix is reducing the work per request, through caching, removing slow plugins or upgrading the server.
Also watch system memory. If the server starts using swap heavily or the kernel’s out-of-memory killer terminates processes, often the database, you’ve allowed too many workers.
| dmesg | grep -i "killed process"</code> |
|---|
Enable the status page
PHP-FPM has a built-in status page showing active, idle and queued processes. Enable it in the pool configuration:
Then expose it in Nginx, restricted to your own IP address or localhost only. During busy periods, the status page shows exactly how close you are to the worker limit and whether requests are waiting.
Use the slow log
A slow log records PHP requests that take longer than a set time, with a stack trace showing where the time went:
This is one of the best tools for finding slow plugins. If a particular function or plugin keeps appearing in the slow log, you’ve found your bottleneck.
Timeouts
request_terminate_timeout sets the maximum time a single request can run before PHP-FPM kills it. Set it a little higher than your PHP max_execution_time and your Nginx fastcgi_read_timeout, so the layers fail in a predictable order. Without it, runaway requests can tie up workers indefinitely.
Long-running tasks, like large imports or backups, are better run through WP-CLI than through web requests, which avoids timeout problems entirely.
Memory limits inside WordPress
PHP’s memory_limit setting caps how much memory a single request can use. WordPress also has its own constants:
WP_MEMORY_LIMIT applies to the front end, and WP_MAX_MEMORY_LIMIT to admin tasks like image processing. These raise WordPress’s limit, but they can’t exceed what the server allows.
Remember that memory_limit is a ceiling, not an allocation. A process limited to 256 MB doesn’t use 256 MB; it can use up to that. Your max_children calculation should be based on measured averages, not on the limit.
Don’t forget OPcache
OPcache stores compiled PHP code in memory, so WordPress files don’t have to be compiled on every request. It’s one of the biggest free performance boosts available. Make sure it’s enabled and sized properly:
If OPcache’s memory fills up, it starts discarding cached files, and performance drops. On servers with many sites, increase memory_consumption and max_accelerated_files accordingly. An OPcache status script can show memory usage and hit rate.
Caching reduces the need for workers
The most effective way to handle more traffic isn’t adding PHP workers. It’s needing fewer of them. Page caching serves anonymous visitors without PHP at all. A persistent object cache like Redis makes each PHP request faster, so workers are freed sooner. Faster requests mean the same number of workers can serve many more visitors.
Apply changes safely
After editing pool settings:
Test the configuration first, then reload. Watch memory, the status page and logs for a day or two after changes, especially during your busiest hours.
A worked example from a real setup
On a 4 GB server running one busy WooCommerce store and two small brochure sites, I measured the store’s PHP processes averaging around 95 MB under normal load, and the brochure sites around 55 MB. After reserving memory for MariaDB, Redis and the system, roughly 2.2 GB remained for PHP. I gave the store a dynamic pool with a maximum of 18 workers and each brochure site an ondemand pool with a maximum of 4. Under a sale, the store’s status page showed it peaking at 15 active workers with no queue, and memory stayed comfortably out of swap. The numbers will be different for your server, but the method is the same: measure, reserve, divide and then watch.
Frequently asked questions
What’s a good pm.max_children value?
There’s no universal number. It depends on your server’s memory and how much each WordPress process uses. Measure, calculate and adjust based on real behaviour.
Should I use static or dynamic?
Dynamic suits most servers. Static can be slightly faster for a single high-traffic site on a dedicated server. Ondemand is good for many low-traffic sites sharing one server.
Why do I get 502 errors under load?
Often because all PHP workers are busy and requests are timing out, or because PHP-FPM crashed after running out of memory. Check the PHP-FPM log and system memory.
Will raising max_children fix a slow site?
Only if requests are queuing and you have spare memory. If individual pages are slow even with no queue, the problem is the code or database, not the number of workers.
Final thoughts
PHP-FPM tuning is about matching workers to memory. Measure how much memory your WordPress processes really use, calculate a safe maximum, choose a process manager that suits your sites and watch the logs and status page for signs of strain. Combine that with OPcache and good caching, and your server will handle far more traffic, far more smoothly.




