Optimize PHP-FPM for WordPress on a VPS

The most common cause of a slow WordPress site on a VPS isn't the database or the web server, it's an untuned PHP-FPM pool. The default config from the package manager is safe for shared hosting. On a single VPS with full root access, it wastes memory and chokes on traffic spikes. This post walks through every setting that matters for WordPress, pool size, process limits, request termination, and opcache, on Ubuntu 24.04 with PHP 8.3. Each change comes with a verify command so you can see the effect immediately.
Prerequisites
- A VPS running Ubuntu 24.04 with root or sudo access.
- LEMP stack installed (Nginx + PHP-FPM + MySQL/MariaDB) plus WordPress already serving a site.
- PHP 8.3 (the default for Ubuntu 24.04). Check with
php -v. - Basic familiarity with
nanoorvimand thesystemctlcommand.
Why PHP-FPM tuning matters before caching
Many administrators jump straight to a caching plugin (W3 Total Cache, WP Rocket, Litespeed Cache) and think the job is done. Caching reduces PHP requests for anonymous visitors, but WordPress still runs PHP for logged-in users, admin pages, REST API calls, WooCommerce cart operations, and dynamic content. An untuned pool hits 502 Bad Gateway or 503 Service Temporarily Unavailable errors when traffic exceeds the worker count. Worse, if pm.max_children is set too high, the server runs out of RAM and the OOM killer terminates random processes, including MySQL or Nginx.
The goal is a pool that uses as much of your VPS memory as safely possible, without letting a single slow request hog a worker forever. Every setting below is tested on a 2 GB RAM VPS (the most common WordPress start size) with notes on what to change for 4 GB or 8 GB.
Step 1, Locate the pool config and take a baseline
The PHP-FPM pool for Nginx is normally at /etc/php/8.3/fpm/pool.d/www.conf. Before editing anything, record the current state so you can compare later.
# Show the pool configuration (filter out comments)
grep -v '^;' /etc/php/8.3/fpm/pool.d/www.conf | grep -v '^$'
# Check how many PHP-FPM processes are running right now
ps aux | grep php-fpm | wc -l
# Check how much memory each worker is using (RSS column)
ps -eo pid,rss,comm | grep php-fpm | awk '{sum+=$2} END {print "Total RSS (MB):", sum/1024}'
Write down the current pm.max_children and the total RSS. On a default installation, you'll see pm = dynamic with pm.max_children = 5, pointlessly low for a VPS.
Step 2, Choose the process manager mode: dynamic, ondemand, or static
The pm directive sets how PHP-FPM manages children. For a WordPress VPS, the choice depends on traffic pattern and RAM.
| Mode | Behavior | Best for |
|---|---|---|
dynamic | Keeps a pool of idle workers ready. Scales up to pm.max_children and down to pm.min_spare_servers. | Steady traffic, admin panels, WooCommerce. |
ondemand | Spawns workers only when a request arrives, kills idle ones after a timeout. Saves memory during quiet hours. | Low-traffic or bursty sites where you want the smallest possible baseline memory footprint. |
static | Fork a fixed number of children at startup. No spawning overhead, no scaling. | Predictable high-traffic sites (conference registration, ticket sales). |
My recommendation for most WordPress VPSs: dynamic. It gives you a guard against traffic spikes without wasting RAM when nobody visits. Switch to ondemand only if your VPS has 1 GB RAM or less and you need every megabyte. Switch to static only when the traffic graph is a flat line.
Step 3, Calculate pm.max_children from available RAM
This is the single most important PHP-FPM setting. Set it too low: 503 errors. Set it too high: OOM. Here is the formula:
pm.max_children = (Total RAM - MySQL overhead - OS overhead - other services) / Average PHP worker RSS
On a 2 GB VPS with Nginx + MariaDB + WordPress running one site, you typically have about 1.2 GB available after MySQL uses 400 MB and the OS uses 400 MB. A single PHP-FPM worker for WordPress uses roughly 40-50 MB RSS (measured on PHP 8.3 with opcache enabled).
# Estimate safely: 1200 MB / 50 MB = 24 children
The default of 5 children means your server sits 80% idle while WordPress visitors get timeouts. Raise it to at least 16-24 for 2 GB, 32-48 for 4 GB, 64-80 for 8 GB. Always test: set 24, run ps aux | grep php-fpm | wc -l after a traffic burst, and watch for swap usage with free -h.
Step 4, Configure a sensible www.conf for WordPress
Open the pool file:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
Find and change these lines. Values shown are for a 2 GB VPS.
pm = dynamic
pm.max_children = 24
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
request_terminate_timeout = 120
request_slowlog_timeout = 5
slowlog = /var/log/php8.3-slow.log
What each setting does:
pm.start_servers, how many children launch when PHP-FPM starts. Proportional topm.max_children: 4 is fine for 24.pm.min_spare_servers / pm.max_spare_servers, the pool keeps 2-6 idle workers ready. Low enough to not waste RAM, high enough to absorb a sudden hit.pm.max_requests = 500, each worker processes up to 500 requests before dying and being replaced. This prevents memory fragmentation from a long-lived PHP script with a small leak.request_terminate_timeout = 120, any request running longer than 120 seconds gets killed. Without this, a stuck WP-Cron job or a plugin that hangs an HTTP call can hold a worker forever, collapsing the pool.request_slowlog_timeout = 5, logs any request that takes more than 5 seconds to the slow log. This is your early warning for a problematic plugin or a slow external API call.
Save and restart:
sudo systemctl restart php8.3-fpm
sudo systemctl status php8.3-fpm
Step 5, Tighten the PHP memory and execution limits
WordPress and most plugins respect memory_limit set in php.ini. The default is often 128 MB, which is enough for a basic site but too low for a page builder (Elementor, Divi) or a heavy WooCommerce store.
sudo nano /etc/php/8.3/fpm/php.ini
Change these:
memory_limit = 256M
max_execution_time = 120
max_input_time = 120
upload_max_filesize = 64M
post_max_size = 80M
256 MB is a solid ceiling for a single request. If you run a media-heavy site or allow large theme ZIP uploads, raise upload_max_filesize and post_max_size accordingly. Restart PHP-FPM again.
sudo systemctl restart php8.3-fpm
Verify the new limit: create a PHP file in your WordPress root (/var/www/html/info.php) with <?php phpinfo();, then visit https://yourdomain.com/info.php. Find memory_limit and max_execution_time. Delete the file after checking, it exposes configuration details to the public.
Step 6, Enable and tune Opcache
Opcache caches compiled PHP bytecode in shared memory. Without it, PHP re-parses every WordPress file on every request. Enabling opcache is the highest-ROI performance change after raising pm.max_children.
The opcache config is in /etc/php/8.3/mods-available/opcache.ini (it may also be set in the php.ini file). Open it and ensure these values are present and uncommented:
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
Settings explained:
opcache.memory_consumption = 128, 128 MB of shared memory for cached scripts. WordPress has about 1000-1500 PHP files. 128 MB is plenty. Monitoropcache_get_status()later and raise to 192 MB if the cache fills up.opcache.max_accelerated_files = 4000, the number of PHP files opcache can track. With plugins and themes, WordPress counts reach 2000-3000. 4000 leaves headroom.opcache.revalidate_freq = 60, check file timestamps every 60 seconds. After a plugin update or a code deployment, opcache only revalidates after 60 seconds (or after a PHP-FPM restart). For development, set this to 2. For production, 60-300 is fine.opcache.fast_shutdown = 1, faster process cleanup, reduces latency.
Restart PHP-FPM again and verify that opcache is active:
sudo systemctl restart php8.3-fpm
php -i | grep opcache.enable
You should see opcache.enable => On => On. For a more detailed view, install and run the opcache-gui script or use WP CLI with wp cli info.
Step 7, Align Nginx with the new PHP-FPM config
The PHP-FPM pool is only useful if Nginx actually talks to it. Check your Nginx site config (/etc/nginx/sites-available/yourdomain) for the fastcgi_pass line. It should point to a Unix socket, not a TCP port, for lower latency on the same machine:
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
Also set the buffer sizes so Nginx doesn't truncate PHP responses:
fastcgi_buffers 16 32k;
fastcgi_buffer_size 64k;
fastcgi_busy_buffers_size 128k;
Test the config and reload:
sudo nginx -t
sudo systemctl reload nginx
Step 8, Verify the pool under load
After restarting everything, run these checks to confirm the pool is working as expected:
# See active PHP-FPM processes
ps aux | grep php-fpm | grep -v grep
# Check the socket is listening
ss -xl | grep php
# Watch the slow log for any request over 5 seconds
tail -f /var/log/php8.3-slow.log
# Generate traffic to stress the pool (install siege: apt install siege)
siege -c 10 -t 30s https://yourdomain.com
While siege runs, open another SSH session and run free -h and ps aux | grep php-fpm | wc -l. You should see the worker count climb toward pm.max_children (or stop at the limit) without the server swapping. If you hit swap, reduce pm.max_children by 4 and test again.
Troubleshooting
502 Bad Gateway after changing config
The socket path in your Nginx config doesn't match the one PHP-FPM is using. Run sudo ls /var/run/php/ to see the actual socket name, then update fastcgi_pass in your Nginx config.
503 Service Unavailable intermittently
The pool is maxed out. Look at the slow log first (/var/log/php8.3-slow.log). If a plugin is consistently taking >5 seconds, fix it. If requests are fast but the pool is still full, raise pm.max_children, but only if you have free RAM. Check with free -h.
Opcache not caching files
Run php -r "print_r(opcache_get_status());". If cache_full is false but num_cached_scripts stays low, the opcache.validate_timestamps may be set to 1 with a very short revalidate_freq, or a plugin like Query Monitor explicitly clears opcache. Also verify the config file was actually loaded, check php -i | grep "opcache" for the correct ini file path.
FAQ
How often should I recalculate pm.max_children?
After every major WordPress update, plugin change, or migration to a different VPS plan. The memory footprint of a PHP worker can shift by 10-15 MB between versions. Run the ps -eo pid,rss,comm | grep php-fpm command once a month to stay current.
Is dynamic mode always better than ondemand for a VPS?
Not always. If your VPS has 1 GB RAM and serves a low-traffic site, ondemand can save 150-200 MB by keeping only 1-2 workers alive during quiet hours. The trade-off is a slight latency penalty when a visitor hits a cold worker, but on a Linux VPS, that penalty is about 100-200 ms, which most users don't notice.
Does opcache conflict with a WordPress caching plugin?
No. Opcache caches PHP bytecode (the compiled result of your PHP files). A caching plugin caches the HTML output of page requests. They solve different problems and work together. Enable both.
What is the slowlog telling me that a PHP error log doesn't?
A slowlog shows which requests took too long, even if they completed without errors. A PHP error log only shows fatal errors. A slowlog catches a WooCommerce checkout that takes 7 seconds because an external payment gateway is hanging, no error, but a terrible user experience.
Should I use a Unix socket or a TCP port for fastcgi_pass in Nginx?
Use a Unix socket when PHP-FPM and Nginx run on the same host. It avoids TCP stack overhead and is measurably faster. Use a TCP port only when PHP-FPM runs on a different server (rare for a single VPS).
Related articles
- nginx tuning for high traffic worker gzip buffer cache
- configure swap and optimize memory on a low ram VPS
- making WordPress fast with LiteSpeed and LSCache on a VPS
- install aapanel on Linux Ubuntu 24.04 or Debian 12 VPS


