Optimize PHP-FPM: Workers, Pools & OPcache on VPS
PHP-FPM (FastCGI Process Manager) is a PHP implementation designed for high performance, typically paired with Nginx (or Apache with mod_proxy_fcgi). The advantage of PHP-FPM is the ability to scale the number of worker processes based on workload, allowing it to handle more requests than mod_php, which ties PHP processes directly to Apache. Proper PHP-FPM tuning can make WordPress sites 2–3x faster without needing to add more RAM.
Understanding pm.max_children
The most critical variable in PHP-FPM is pm.max_children, which sets the maximum number of PHP worker processes that can run simultaneously. Set it too high and you'll trigger the OOM Killer due to running out of RAM; set it too low and requests will queue up, causing timeouts.
Basic calculation formula:
# Check how much RAM each PHP-FPM process uses on average (in MB)
ps aux | grep "php-fpm" | awk '{sum+=$6; count++} END {print sum/count/1024 " MB average"}'
# Example: VPS with 2GB RAM, OS+Nginx uses ~300MB, leaving ~1700MB
# PHP worker uses avg 50MB → max_children = 1700/50 = 34
| VPS RAM | pm.max_children (Recommended) | Notes |
|---|---|---|
| 1 GB | 8–12 | Good for small sites with low traffic |
| 2 GB | 20–30 | WordPress with 1–3 sites |
| 4 GB | 40–60 | Multiple WordPress sites or WooCommerce |
| 8 GB | 80–120 | High-traffic sites or PHP API servers |
PHP-FPM Configuration File Location
# Ubuntu/Debian (PHP 8.2)
/etc/php/8.2/fpm/pool.d/www.conf
# AlmaLinux/Rocky Linux (PHP 8.2)
/etc/php-fpm.d/www.conf
# Open and edit
nano /etc/php/8.2/fpm/pool.d/www.conf
Recommended Pool Settings for 2GB VPS
[www]
user = www-data
group = www-data
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
; Process Manager in dynamic mode (adjusts workers based on load)
pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500 ; Restart worker after 500 requests (prevents memory leaks)
; Timeout
request_terminate_timeout = 60s
request_slowlog_timeout = 10s
slowlog = /var/log/php/slow.log
Choose pm Mode for Your Workload
- pm = dynamic — Automatically scales workers based on load. Good for typical websites
- pm = ondemand — Creates workers only when requests arrive. Good for low-RAM VPS with variable traffic
- pm = static — Fixed number of workers at all times. Good for high-RAM VPS with consistent high traffic. Fastest response time
Configure OPcache — Maximum Speed
OPcache is a bytecode cache that converts PHP scripts to bytecode once and keeps it in memory, eliminating the need to parse PHP files on every request. Just enabling OPcache can make WordPress 40–70% faster without any code changes.
# Edit /etc/php/8.2/fpm/conf.d/10-opcache.ini or /etc/php/8.2/mods-available/opcache.ini
[opcache]
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256 ; RAM for bytecode cache (MB)
opcache.interned_strings_buffer=16 ; String cache buffer (MB)
opcache.max_accelerated_files=10000 ; Number of PHP files to cache
opcache.revalidate_freq=2 ; Check file changes every 2 seconds
opcache.fast_shutdown=1
opcache.validate_timestamps=1 ; Keep on during development
; opcache.validate_timestamps=0 ; Enable on production for maximum speed (must restart php-fpm after deploy)
opcache.validate_timestamps=0 on production servers to skip file validation on every request. However, you must run systemctl reload php8.2-fpm after every code deployment.
Configure realpath_cache — One More Level
# /etc/php/8.2/fpm/php.ini
[PHP]
realpath_cache_size = 4096K ; Cache PHP file path resolution (WordPress has many files)
realpath_cache_ttl = 600 ; Cache for 10 minutes
Create Separate Pools for Each Website
If you have multiple websites on the same VPS, create separate pools for each one so they don't compete for workers and you can monitor performance more accurately:
# Create a new file
cp /etc/php/8.2/fpm/pool.d/www.conf /etc/php/8.2/fpm/pool.d/mysite.conf
# Edit in mysite.conf
[mysite] ; Pool name
user = mysite
group = mysite
listen = /run/php/php8.2-mysite.sock ; Separate socket
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 5
# Update Nginx to use this pool's socket
fastcgi_pass unix:/run/php/php8.2-mysite.sock;
Reload and Verify
# Test config before reloading
php-fpm8.2 -t
# Reload PHP-FPM (doesn't kill running processes)
systemctl reload php8.2-fpm
# Check status
systemctl status php8.2-fpm
# Watch real-time worker count
watch -n 1 "ps aux | grep php-fpm | grep -v grep | wc -l"
Monitor Performance with Status Page
# Add to pool config to enable status page
pm.status_path = /fpm-status
ping.path = /fpm-ping
# Add to Nginx config (localhost access only)
location /fpm-status {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# View status
curl http://localhost/fpm-status?full
Proper PHP-FPM tuning is one of the most cost-effective ways to increase website speed without adding hardware. If you're looking for a VPS with solid performance from the start, asiagb's Thailand VPS offers multiple plan sizes with NVMe SSD storage to help PHP-FPM perform at its best.
Verify PHP-FPM is Working Correctly
# Check config syntax
php-fpm8.2 -t
# Expected output: NOTICE: configuration file /etc/php/8.2/fpm/php-fpm.conf test is successful
# Check service status
systemctl status php8.2-fpm
# Watch real-time worker count
watch -n 1 "ps aux | grep php-fpm | grep -v grep | wc -l"
# Verify OPcache via PHP
php -r "var_dump(opcache_get_status()['opcache_enabled']);"
# Should be: bool(true)
pm.status_path = /fpm-status in your pool config, then run curl http://localhost/fpm-status to see active connections, idle workers, and slow requests in real-time — very useful when traffic spikes.
Common Issues & Troubleshooting
Error: "502 Bad Gateway" from Nginx
Usually caused by a mismatch between the socket path in your Nginx config and the one PHP-FPM is actually using:
# Check what socket PHP-FPM created
ls /run/php/
# Should show php8.2-fpm.sock
# Check Nginx config path
grep fastcgi_pass /etc/nginx/sites-enabled/*.conf
# Must match: unix:/run/php/php8.2-fpm.sock
# Check socket permissions
ls -la /run/php/php8.2-fpm.sock
# Must be writable by www-data
Error: "WARNING: [pool www] seems busy (pid X, fpm_got_signal)" with heavy requests
pm.max_children is too low for your traffic — PHP workers are exhausted and requests are queuing:
# Check if workers are maxed out
curl http://localhost/fpm-status
# Compare active processes against max children
# Increase max_children in pool config
nano /etc/php/8.2/fpm/pool.d/www.conf
# Increase pm.max_children
systemctl reload php8.2-fpm
Error: PHP-FPM won't start — "WARNING: Nothing matches the include pattern '/etc/php/8.2/fpm/pool.d/*.conf'"
The pool config file may have been deleted or the directory is empty. Create a pool:
# Check pool directory
ls /etc/php/8.2/fpm/pool.d/
# Must have at least one .conf file
# If empty, restore default
cp /etc/php/8.2/fpm/pool.d/www.conf.default /etc/php/8.2/fpm/pool.d/www.conf
# Or reinstall: apt install --reinstall php8.2-fpm
Error: WordPress still slow even after enabling OPcache
Check if OPcache is fully loaded and that validate_timestamps isn't causing repeated rechecks on every request:
# View OPcache stats
php -r "print_r(opcache_get_status());"
# Check opcache_statistics: hits vs misses
# Check memory usage
php -r "print_r(opcache_get_status()['memory_usage']);"
# If used_memory is near full, increase opcache.memory_consumption
Error: pm.max_requests causes frequent worker restarts and slowdowns
pm.max_requests = 500 means workers restart every 500 requests to prevent memory leaks. If restarts happen too frequently, either increase the value or find and fix memory leaks in your code:
# Check memory size of each worker
ps aux | grep php-fpm | awk '{print $6/1024 " MB " $11}' | head
# If memory isn't leaking, increase max_requests to 2000–5000
# pm.max_requests = 2000
Frequently Asked Questions
How do I switch PHP versions without downtime?
Install the new PHP version alongside the current one, create a new pool for it, then change Nginx to point the socket to the new version. Test that website first before migrating others one at a time — both old and new PHP versions can run in parallel on Ubuntu.
Does every site need its own PHP-FPM pool?
Not required, but recommended if you have multiple sites on the same VPS. Separate pools give each site its own workers, so a traffic spike on one site won't affect the others. Each pool also runs as a different user, which improves security.
What's the difference between pm = dynamic and pm = ondemand?
Dynamic keeps minimum workers running all the time (pm.start_servers), so the first request responds faster. It's good for steady traffic. Ondemand creates workers only when requests arrive, then kills idle ones. It's better for low-RAM VPS or unpredictable traffic, but the first request after idle will be slightly slower.