How to Tune PHP-FPM for WordPress and WooCommerce

A flat vector illustration of the WordPress logo integrated into a high-speed speedometer dial. Text: TUNE PHP-FPM FOR WORDPRESS

We may earn a referral fee at no additional cost to you when you shop through the links on this page.

Your WooCommerce store crawls or starts throwing 502 errors the moment traffic climbs, yet top shows spare CPU and the database looks idle. Nine times out of ten the bottleneck is an untuned PHP-FPM pool: too few workers to absorb the concurrency, or so many that the server runs out of memory and starts swapping.

Prerequisites 

This is a server-level guide. You will need:

  • Root or sudo SSH access to the server.
  • PHP-FPM 8.x already installed and serving the site (nginx, or Apache with mod_proxy_fcgi).
  • A WordPress or WooCommerce site running under real or anticipated traffic.

What the PHP-FPM Pool Settings Actually Control 

PHP-FPM serves your site from a pool of worker processes, and each worker handles exactly one request at a time. This guide is PHP-FPM specific: pool tuning has no equivalent under Apache’s older mod_php, so if your host still runs mod_php these settings will not apply. Tuning is really just two decisions: how many workers you allow, and how the pool decides when to start and stop them. Six directives cover almost everything that matters.

DirectiveWhat it sets
pmThe process manager mode: static, dynamic, or ondemand
pm.max_childrenThe hard ceiling on simultaneous worker processes
pm.start_serversWorkers created at startup (dynamic only)
pm.min_spare_serversMinimum idle workers kept ready (dynamic only)
pm.max_spare_serversMaximum idle workers allowed (dynamic only)
pm.max_requestsRequests a worker handles before it respawns (0 means never)

The pm directive is the first fork in the road, and it accepts one of three values: static, dynamic, or ondemand. The other five directives shape how aggressively the pool scales between idle and busy. Get pm.max_children wrong in either direction and the site suffers, so that is the number to derive from real data rather than copy from a blog.

Step 1: Measure Your Real Average Worker Size 

Every tuning calculator online asks for your average PHP-FPM process size, then never tells you how to find it. You get it straight from the workers already running on your server. Each PHP-FPM worker is a process, so its resident memory (RSS) is a reliable estimate of what one request costs you.

Run this while the site is under normal traffic. It reads the RSS of every php-fpm8.3 worker (use your installed version) and prints the average in megabytes:

Terminal
ps --no-headers -o rss,args -C php-fpm8.3 | awk '/pool/ { sum+=$1; n++ } END { if (n) print int(sum/n/1024) "M"; else print "no php-fpm pool workers running" }'
Output
48M

If the command prints “no php-fpm pool workers running”, the process name does not match your installed version, so swap 8.3 for yours (run systemctl list-units 'php*-fpm.service' to check). Run it a few times across a busy period and take the highest figure you see, not the quietest, because you are sizing for load, not for an idle box.

Measure, don’t guess. That number is your real per-worker cost, taken from your own plugins under your own traffic. A generic “assume 50 MB” is how servers end up either starved of workers or quietly swapping.

Step 2: Calculate pm.max_children From Your RAM 

pm.max_children is the maximum number of PHP-FPM workers that can run at once, and because each worker handles one request at a time, it is effectively your concurrency limit. The RAM formula gives you a ceiling, the most workers your memory can hold without swapping, not the number you should actually run: pm.max_children = (Total RAM - RAM reserved for everything else) / Average worker RAM. Size it against the memory you can spare for PHP, not the server’s total RAM.

“Everything else” is the database, the web server, Redis if you run it, and the operating system itself. On a typical 4 GB VPS running MySQL and nginx, reserving around 1.5 GB for those leaves about 2.5 GB for PHP. At the 48 MB per worker we measured above, that is roughly 2560 / 48, or about 53 workers. That 53 is your raw memory ceiling; round it down for headroom and set pm.max_children = 45 as the value you actually run.

That ceiling is a limit, not a target. On most VPS boxes the CPU cores saturate long before the memory does, and past that point extra workers just queue and fight, so response times get worse rather than better. Treat the RAM figure as the number you must never exceed, then find the real optimum empirically: push realistic load at your checkout, watch response time and free -m together, and raise the worker count only while both keep improving. A benchmarked VPS tune that ran the same WooCommerce store at several pool sizes landed on a sweet spot well below the memory ceiling, with the larger pools measurably slower.

When every worker is busy, further requests wait in a queue. If that queue fills, nginx has nowhere to hand the request and returns a 502 Bad Gateway. It is tempting to fix that by raising pm.max_children, but only raise it if both the RAM math still holds and your cores are not already saturated. Setting it higher than your memory supports is worse than a 502: the server starts swapping, every request slows to a crawl, and eventually the kernel’s OOM killer starts terminating processes.

pm.max_children multiplied by your average worker size must comfortably fit in the RAM you set aside for PHP. A value that looks generous on paper is what pushes a small server into swap.

Step 3: Choose a Process Manager Mode 

The pm mode decides how the pool sits between requests. There is a clear default and two situational alternatives.

ModeHow it behavesBest for
dynamicKeeps a band of idle workers ready and scales up to pm.max_children under loadMost WordPress and WooCommerce sites
ondemandStarts no workers until a request arrives, then kills them after an idle timeoutLow-traffic sites, or many sites sharing one small box
staticRuns a fixed pm.max_children workers at all timesA dedicated high-traffic box where predictable latency matters

In dynamic mode three more directives shape the idle band. pm.start_servers sets how many workers spin up when PHP-FPM starts, while pm.min_spare_servers and pm.max_spare_servers set the minimum and maximum number of idle workers the pool keeps waiting for the next request. Keeping a few spares ready means the first visitor after a quiet spell does not wait for a worker to fork.

ondemand trades that readiness for memory. It starts nothing until traffic arrives, and pm.process_idle_timeout controls how long an idle worker survives before the master process shuts it down to free memory. That is ideal when you host several low-traffic sites on one server and want none of them holding RAM they are not using. static is the opposite bet: it keeps the full worker count resident so there is never a fork delay, which suits a single busy store on its own box.

Start with dynamic. Move to static on a dedicated box where you have measured that fork latency is costing you, or when you want a fixed worker count that is easier to load-test against, and reach for ondemand when RAM is the scarce resource across many small sites.

Step 4: Recycle Workers With pm.max_requests 

A single PHP-FPM worker stays alive across thousands of requests, and a heavy plugin stack slowly leaks memory into it. Left alone, a worker’s RSS creeps upward for hours until the pool is quietly using far more memory than your calculation assumed. pm.max_requests is the safety valve.

Set it, and each worker respawns after handling that many requests, returning its accumulated memory to the system. The PHP manual is blunt about why it exists: it is there to work around memory leaks in third-party libraries. A value of 0 means workers never respawn, which is exactly the wrong choice for a plugin-heavy WooCommerce site. A value between 500 and 1000 keeps memory flat without respawning so often that you lose the benefit of warm workers.

Tuning Profiles: Light WordPress vs Heavy WooCommerce 

A basic WordPress page might run 20 to 40 database queries. A WooCommerce product page with variations, reviews, related products, and dynamic pricing can run 200 to 500 or more queries per page load, and the cart and checkout add session handling, stock checks, tax math, and payment-gateway calls on top. That work lives in the PHP worker, so a WooCommerce worker is simply bigger than a blog’s. Two starting profiles, both for dynamic mode:

Light WordPress Blog (2 GB Server) 

/etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

Heavy WooCommerce Store (4 GB Server) 

/etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500

The WooCommerce box has twice the blog’s RAM but only a few more workers, not double, because each WooCommerce worker is larger and eats more memory. That is the whole point of measuring first: a store runs fewer workers per gigabyte than a blog does, so sizing it by copying a blog’s workers-per-gigabyte ratio would push it into swap. Object caching takes real load off these workers too, so pairing this with Redis object caching for WordPress lets each worker finish faster and serve more requests.

Apply, Reload, and Verify With the FPM Status Page 

The pool file lives at /etc/php/<version>/fpm/pool.d/www.conf on Ubuntu and Debian. If you are unsure which version is active, the PHP-FPM configuration file location guide walks through finding it. Substitute your installed PHP version for 8.3 in the commands below (run systemctl list-units 'php*-fpm.service' if you are unsure which is active). Open it with a text editor:

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

Set your chosen values, and while you are in the file, enable the status page by uncommenting and setting this line. It is the single best way to see whether your numbers are right:

/etc/php/8.3/fpm/pool.d/www.conf
pm.status_path = /status

Save and exit (press CTRL + X, press Y, then press ENTER)

Changes to this file do nothing until you reload the versioned php-fpm service. A reload applies the new configuration without dropping in-flight requests:

Terminal
sudo systemctl reload php8.3-fpm

Confirm the service came back cleanly:

Terminal
systemctl status php8.3-fpm
Output
● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
     Loaded: loaded (/usr/lib/systemd/system/php8.3-fpm.service; enabled)
     Active: active (running)

The status page reports what the pool is actually doing: how many workers are active, how many are idle, and whether requests are backing up in the listen queue. PHP-FPM will not serve /status over HTTP on its own; nginx has to route that path to the FPM socket. Add this location block to your site’s nginx server block, locked to localhost so the pool’s internals are never exposed publicly. On Apache with mod_proxy_fcgi the idea is identical: pass /status to the FPM socket with a localhost-restricted <LocationMatch> and SetHandler, then reload Apache instead of nginx.

/etc/nginx/sites-available/devanswers.test
location = /status {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Reload nginx so the new location takes effect, then read the page from the server itself with curl:

Terminal
sudo systemctl reload nginx
curl -s -H "Host: devanswers.test" http://127.0.0.1/status
Output
pool:                 www
process manager:      dynamic
idle processes:       3
active processes:     2
total processes:      5
listen queue:         0

A healthy pool shows a listen queue at or near zero and active processes comfortably below pm.max_children. If active processes are pinned at the ceiling and the listen queue is climbing, your workers are the bottleneck and it is time to revisit Step 2.

Troubleshooting: 502s, Reached max_children, and Swapping 

Three failure patterns cover almost every PHP-FPM tuning problem. The PHP-FPM log at /var/log/php8.3-fpm.log (or journalctl -u php8.3-fpm) is where the pool tells you which one you have.

SymptomCauseFix
502 Bad Gateway under loadEvery worker busy, the request queue filled, nginx had nowhere to send the requestRaise pm.max_children only if the RAM math allows; otherwise the server is undersized
server reached max_children setting in the logThe ceiling is too low for your concurrencyRe-measure worker size, recalculate, and raise the ceiling within your memory budget
Server swapping or OOM killspm.max_children times worker size exceeds available RAMLower pm.max_children or add RAM; never trade swap for concurrency

The exact warning PHP-FPM logs when it hits the ceiling looks like this, and it names the current value so you know where you started:

Output
WARNING: [pool www] server reached max_children setting (5), consider raising it

Treat that message as a prompt to measure, not a licence to keep raising the number until the warning stops. A ceiling that silences the warning but sends the box into swap has made the site slower, not faster.

When Tuning Isn’t Enough 

I have tuned PHP-FPM pools on servers from 1 GB up to 32 GB, and there is a point on the small end where the real answer is not another config edit. If you have measured your worker size, sized pm.max_children to your RAM, capped pm.max_requests, and the pool still pins at its ceiling during ordinary traffic, you have outgrown the server. Adding RAM or moving to a bigger box is the real fix, and pairing pool tuning with PHP OPcache and object caching buys you the most from whatever hardware you are on.

There is also a point where you simply should not be hand-tuning www.conf at all. If PHP-FPM is not your job and every hour spent on it is an hour not spent on the business, a managed hosting platform like Cloudways sizes and monitors the pool for you, scaling the workers as your traffic grows so a checkout never stalls because a number in a config file was too low. Tuning it yourself is worth doing once so you understand what you are paying someone else to stop worrying about. If you are deciding between manual control and convenience, this comparison of Cloudways vs DigitalOcean for WordPress benchmarks the performance of both.

Support DevAnswers
Written, tested and maintained by one developer. Your support keeps these guides accurate and ad-light.
Buy me a coffee

Comments

Leave a reply