A fresh PHP-FPM install ships with pool settings sized for nothing in particular. The default pm.max_children value is a generic placeholder, not a number calculated from your VPS's actual RAM or your site's actual PHP process size, and OPcache, if enabled at all by default, is usually running with conservative settings that undersell what it can do. Left as-is, PHP-FPM either wastes available RAM by running fewer workers than your server could support, or gets oversubscribed under a traffic spike and starts queuing requests because it was never told how many concurrent processes it could safely run.
This guide covers calculating the right number of PHP-FPM worker processes for your specific VPS, choosing the process manager mode that fits your traffic pattern, and configuring OPcache so PHP stops parsing and compiling the same files on every single request. By the end, PHP-FPM will be sized deliberately for your hardware instead of running on defaults nobody chose on purpose.
This assumes you have completed the WordPress on Nginx and PHP-FPM guide and have a working WordPress install on PHP 8.3.
PHP-FPM's pm directive controls how worker processes are managed, and the three modes fit different traffic patterns. dynamic keeps a baseline of idle workers ready and spawns more under load up to a defined maximum, which suits most WordPress sites with some regular traffic. static keeps a fixed number of workers running at all times, using more RAM constantly but avoiding the small latency cost of spawning a new worker under a sudden spike; it fits sites with steady, predictable, higher traffic. ondemand starts with zero workers and spins them up only when a request arrives, which conserves RAM on a low-traffic site at the cost of a small delay on the first request after a period of inactivity.
For a typical WordPress site on a VPS, dynamic is the right default; it is what the rest of this guide assumes. Switch to static only once you have confirmed through monitoring that traffic is consistently high enough that dynamic's spawn-and-kill cycle is itself showing up as a cost.
The right number of worker processes depends on how much RAM each one actually uses, which varies by site depending on which plugins and theme are loaded. Measure it directly rather than guessing. With the site under normal use, check the resident memory of running PHP-FPM workers:
ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; count++} END {print sum/count/1024 " MB average"}'
Output:
58.3 MB average
A typical WordPress site with a handful of common plugins usually lands somewhere between 40MB and 80MB per worker; a site with WooCommerce and several heavier plugins can run higher. This number is what the next step's calculation is based on, so measure your own site rather than assuming a figure.
pm.max_children should reflect how many PHP processes your VPS can run simultaneously without running out of memory, after leaving headroom for the operating system, Nginx, and MariaDB. A reasonable starting point reserves around 1GB for the OS, Nginx, and MariaDB combined on a small to mid-sized VPS, and divides the remainder by your measured average process size.
Using the 58MB measured above, here is how that works out across Arcadia's own VPS plans:
These are ceilings, not targets to fill deliberately; running near the calculated maximum under sustained load means you are one traffic spike away from memory pressure. Setting pm.max_children to roughly 80% of the calculated ceiling leaves a buffer without leaving significant capacity unused.
Open the pool configuration file:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
Update the pm block with your calculated value. For the 4GB example above, using 80% of the ceiling:
pm = dynamic
pm.max_children = 42
pm.start_servers = 8
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.start_servers sets how many workers spawn immediately when PHP-FPM starts. pm.min_spare_servers and pm.max_spare_servers keep a buffer of idle workers ready between requests, so a new request does not always have to wait for a fresh process to spawn. Replace the numbers with values scaled from your own max_children calculation; a common rule of thumb is min_spare_servers around 20% of max_children and max_spare_servers around 35%.
Restart PHP-FPM to apply the new pool settings:
sudo systemctl restart php8.3-fpm
Confirm the service came back up cleanly:
sudo systemctl status php8.3-fpm
Count how many worker processes are currently running to confirm start_servers took effect:
ps --no-headers -C php-fpm8.3 | wc -l
With the pool sized correctly and confirmed running, the next step turns to OPcache, which reduces how much work each of those workers has to do per request.
Without OPcache, PHP parses and compiles every PHP file on every single request, discarding the compiled result the moment the request finishes. OPcache keeps compiled bytecode in shared memory across requests, so WordPress core, theme, and plugin files are compiled once and reused until they change on disk. Open the OPcache configuration:
sudo nano /etc/php/8.3/fpm/conf.d/10-opcache.ini
Set the following values:
opcache.enable=1
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.validate_timestamps=1
opcache.memory_consumption at 192MB gives a typical WordPress install with a moderate plugin count comfortable headroom to cache every file without evicting entries under normal use. opcache.max_accelerated_files needs to exceed the total number of PHP files across WordPress core, your theme, and all active plugins; 10000 covers this for the large majority of sites. opcache.validate_timestamps=1 with a revalidate_freq of 60 means OPcache checks whether a file has changed on disk at most once every 60 seconds rather than on every request, which is a sensible middle ground between performance and not having to manually clear the cache every time you edit a file.
Restart PHP-FPM to apply the OPcache settings:
sudo systemctl restart php8.3-fpm
Create a temporary diagnostic file in the web root to confirm OPcache is running and caching files:
echo '<?php var_dump(opcache_get_status(false));' | sudo tee /var/www/yoursite.com/public/opcache-check.php
Visit https://yoursite.com/opcache-check.php in a browser. A working configuration shows opcache_enabled as bool(true) along with counters for cached scripts and memory usage. This file exposes internal server details and should not be left in place; remove it as soon as you have confirmed the output:
sudo rm /var/www/yoursite.com/public/opcache-check.php
PHP-FPM is now running a worker count calculated from your VPS's actual RAM rather than an untouched default, and OPcache is caching compiled PHP across requests instead of recompiling it every time. Revisit the pm.max_children calculation if you upgrade your VPS plan or notice average process size has changed after adding heavier plugins. From here, Redis object caching addresses the other major source of repeated work on a WordPress site: database queries, rather than PHP execution itself.
Move a live WordPress site from shared hosting to a VPS: database export with my...
Step-by-step: configure a fresh Ubuntu 24.04 VPS from first SSH login to a secur...
How to configure UFW and Fail2Ban on Ubuntu to protect your VPS from port scans...