PHP-FPM (FastCGI Process Manager) is how PHP runs behind Nginx. The two don’t talk to each other directly — Nginx hands PHP requests to PHP-FPM via FastCGI, PHP-FPM processes them, and sends responses back. Getting this configuration right affects both performance and stability.
I’ve set this up dozens of times. Here’s what I’ve learned about the configuration that actually matters.
Basic Setup
Install on Ubuntu/Debian:
apt install nginx php8.2-fpm
PHP-FPM installs with a default pool configuration at /etc/php/8.2/fpm/pool.d/www.conf. You can use the default pool or create custom pools per application.
Nginx Configuration for PHP-FPM
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
try_files $uri =404; in the PHP location is important — without it, you can potentially execute arbitrary files as PHP if the path doesn’t exist (file upload exploit). Always check that the PHP file actually exists before passing to FPM.
fastcgi_pass unix:/run/php/php8.2-fpm.sock; — using a Unix socket instead of a TCP port (like 127.0.0.1:9000) is faster for local communication. Less overhead than TCP for local connections.
PHP-FPM Pool Configuration
The pool config at /etc/php/8.2/fpm/pool.d/www.conf controls how PHP-FPM manages processes. The key section:
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500
pm = dynamic — the process manager adjusts child count based on load. Alternatives: static (fixed number of processes), ondemand (spawn on demand, kill when idle).
pm.max_children — the most important setting. Maximum number of PHP processes. Each process uses memory (typically 30–100MB depending on the application). Set this based on available RAM.
Simple formula:
max_children = (Total RAM - RAM for other services) / Average PHP process memory
If you have 2GB RAM, 500MB used by OS/Nginx/MySQL, and each PHP process uses ~50MB:
(2000 - 500) / 50 = 30 max_children
Check actual PHP process memory:
ps --no-headers -o rss,pid -C php-fpm8.2 | awk '{ sum+=$1; n++ } END { print sum/n " KB average, " sum " KB total" }'
pm.max_requests — restart worker processes after handling this many requests. Prevents slow memory leaks from accumulating indefinitely. 500 is conservative; some apps handle 5000+ without issues.
Process Manager Modes
dynamic — best for most web servers. Starts pm.start_servers processes, keeps between pm.min_spare_servers and pm.max_spare_servers idle, up to pm.max_children total.
pm = dynamic
pm.max_children = 30
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 8
static — always keeps exactly pm.max_children processes running. Best for consistent traffic where you want predictable memory usage and no spawn latency:
pm = static
pm.max_children = 20
ondemand — spawns processes when needed, kills them when idle. Memory efficient for low-traffic or development servers:
pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s
I use dynamic for most production servers and ondemand for low-traffic sites to save memory.
PHP-FPM Status Page
Enable the status endpoint for monitoring:
# In pool config
pm.status_path = /status
Add to Nginx config (restrict access):
location ~ ^/status$ {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
Access it:
curl http://127.0.0.1/status
Output shows active/idle processes, accepted connections, slow requests. Useful for diagnosing performance issues.
For full JSON output:
curl http://127.0.0.1/status?json
Slow Log
Enable logging of slow requests:
; In pool config
slowlog = /var/log/php8.2-fpm-slow.log
request_slowlog_timeout = 5s
Any PHP request taking longer than 5 seconds generates a stack trace in the slow log. This is invaluable for finding performance bottlenecks — not all slow requests are slow at the DB level.
Socket vs TCP
Unix socket (local only):
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
TCP (local or remote):
listen = 127.0.0.1:9000
Socket is faster for local Nginx-to-FPM communication — avoids TCP overhead. TCP is necessary if FPM runs on a different server than Nginx.
PHP.ini Tuning
A few PHP settings that affect performance:
; /etc/php/8.2/fpm/php.ini
; OpCache settings (crucial for performance)
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60
opcache.fast_shutdown = 1
; Memory
memory_limit = 256M
; Execution time
max_execution_time = 30
; Upload settings
upload_max_filesize = 64M
post_max_size = 64M
; Error logging (not display — don't show errors to users in production)
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
OPcache is the single biggest PHP performance improvement. It caches compiled PHP bytecode in memory, skipping the compilation step on every request. Without it, every request parses and compiles PHP files from scratch. With it, that work happens once per deploy.
opcache.revalidate_freq = 60 — check for file changes every 60 seconds. Set to 0 in development for instant changes; set higher (or use validate_timestamps = 0) in production if you know when files change.
Multiple Pools for Different Applications
Instead of one shared pool, I create per-application pools:
# /etc/php/8.2/fpm/pool.d/app1.conf
[app1]
user = app1 group = app1 listen = /run/php/php8.2-app1.sock listen.owner = www-data listen.group = www-data pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 5
This gives each application its own process pool with resource limits. If one app has a slow query that spawns 20 processes, it doesn’t starve other applications. Good isolation on shared servers.
Checking FPM Status
systemctl status php8.2-fpm
php-fpm8.2 -t # Test config syntax
journalctl -u php8.2-fpm -n 50
When something breaks — and it will — the FPM error log is the first place to check:
tail -f /var/log/php8.2-fpm.log
For Nginx + PHP-FPM issues, check both logs. Nginx usually shows “502 Bad Gateway” when FPM is down or overloaded; the FPM log shows why FPM had problems.
For the Nginx side of this configuration, the Nginx SSL setup guide covers adding HTTPS to your Nginx server blocks.