Logo

PHP-FPM: pools, tuning and healthcheck

PHP-FPM: pools, tuning and healthcheck

Configure PHP-FPM for production: dedicated per-site pools, memory management, opcache, healthcheck, slow log. The key to a fast and stable PHP server.

Introduction

PHP-FPM (FastCGI Process Manager) is the standard way to run PHP in prod behind Nginx or Apache. It handles:

  • Pre-forked PHP worker pool
  • Memory recycling (max_requests)
  • Per-user / per-site isolation
  • Slow log to identify slow requests
  • Healthcheck for load balancer

Bad config = 502 Bad Gateway, OOM, wasted RAM. Good config = stability and performance.

Prerequisites

  • Linux VPS Debian / Ubuntu
  • Nginx or Apache already installed
  • Basic PHP knowledge

Step 1: Installation

sudo apt update
sudo apt install -y php8.2-fpm php8.2-mysql php8.2-redis php8.2-curl php8.2-mbstring php8.2-xml php8.2-zip php8.2-gd php8.2-intl
sudo systemctl enable --now php8.2-fpm

Step 2: Pool anatomy

/etc/php/8.2/fpm/pool.d/www.conf:

[www]
user = www-data
group = www-data

listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500

pm.status_path = /fpm-status
ping.path = /fpm-ping

slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s

request_terminate_timeout = 60s

Step 3: Process Manager (pm)

3 modes:

  • static: fixed worker count. Predictable, stable RAM.
  • dynamic: varies between min and max. Good balance.
  • ondemand: created on demand. RAM-efficient, higher latency.

Prod recommendation: dynamic.

Step 4: max_children calculation

Formula: pm.max_children = (available RAM) / (avg RAM per worker)

ps -ylC php-fpm8.2 --sort:rss | head

RSS column in KB. If avg 50 MB and 4 GB RAM for PHP:

pm.max_children = 4096 / 50 = ~80

Stay conservative: 60-70.

Step 5: One pool per site

sudo nano /etc/php/8.2/fpm/pool.d/site1.conf
[site1]
user = site1
group = site1
listen = /run/php/php8.2-site1.sock
listen.owner = www-data
listen.group = www-data

pm = dynamic
pm.max_children = 20
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5

slowlog = /var/log/php-fpm/site1-slow.log
request_slowlog_timeout = 3s

php_admin_value[open_basedir] = /var/www/site1:/tmp
php_admin_value[upload_tmp_dir] = /var/www/site1/tmp
php_admin_value[session.save_path] = /var/www/site1/sessions
php_admin_value[memory_limit] = 256M
sudo useradd -r -s /bin/false site1
sudo mkdir -p /var/www/site1/{tmp,sessions}
sudo chown -R site1:site1 /var/www/site1
sudo systemctl restart php8.2-fpm

Step 6: Nginx config for the pool

server {
    listen 443 ssl http2;
    server_name site1.com;
    root /var/www/site1;
    index index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-site1.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Step 7: Opcache

/etc/php/8.2/fpm/php.ini:

opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.save_comments=1

In stable prod, set opcache.validate_timestamps=0 (10-15% perf gain, needs systemctl reload after each deploy).

Step 8: JIT (PHP 8+)

opcache.jit=1255
opcache.jit_buffer_size=128M

Gain: 5-15% on compute-intensive PHP.

Step 9: Slow log

slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 3s
sudo tail -f /var/log/php-fpm/slow.log

Identifies precise stack trace at slowdown moment.

Step 10: FPM status + monitoring

pm.status_path = /fpm-status
ping.path = /fpm-ping

Nginx:

location ~ ^/(fpm-status|fpm-ping)$ {
    access_log off;
    allow 127.0.0.1;
    deny all;
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    include fastcgi_params;
}
curl http://localhost/fpm-status?full

Integrate into Prometheus with php-fpm_exporter.

Step 11: Healthcheck for load balancer

curl http://localhost/fpm-ping

Returns pong. Use this path in HAProxy, Traefik, AWS ELB.

Step 12: memory_limit and timeouts

php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 60
php_admin_value[max_input_time] = 60
php_admin_value[post_max_size] = 64M
php_admin_value[upload_max_filesize] = 64M

request_terminate_timeout = 65s

Set request_terminate_timeout slightly higher than max_execution_time.

Troubleshooting

502 Bad Gateway

sudo systemctl status php8.2-fpm
sudo tail -f /var/log/php-fpm/error.log
sudo journalctl -u php8.2-fpm -n 50
ls -la /run/php/php8.2-fpm.sock

"server reached pm.max_children setting"

Saturation. Increase pm.max_children, identify slow queries.

Memory leak

pm.max_requests = 100

"MySQL has gone away"

FPM workers keep long connections. Increase MySQL wait_timeout or use PDO::ATTR_PERSISTENT = false.

File upload limit

Check upload_max_filesize, post_max_size, and Nginx client_max_body_size.

Useful commands

sudo systemctl status php8.2-fpm
sudo systemctl reload php8.2-fpm        # no downtime
sudo php-fpm8.2 -t                       # test config
sudo php-fpm8.2 -tt                      # list pools
watch 'curl -s http://localhost/fpm-status'
ps -eo rss,cmd | grep php-fpm | awk '{sum+=$1} END {print sum/1024 " MB"}'
ps aux --sort=-rss | grep php-fpm | head

Conclusion

A healthy PHP-FPM config gives you:

  • Predictable memory
  • Per-site isolation
  • Visibility on slow requests
  • Healthcheck for load balancer

Going further:

  • Combine with OPcache file cache (cache between restarts)
  • Migrate to RoadRunner for extreme perf (PHP daemon mode)
  • Use FrankenPHP for integrated PHP + web server

Resources

Join our Discord community server

For any questions, suggestions, or just to chat with the community, join us on Discord!

900+Members