Request lifecycle, SAPIs and PHP-FPM

PHP · Expert · 16 min

Capacity planning, timeouts and many production incidents come down to how PHP processes are run.

PHP talks to the outside world through a SAPI (Server API): cli for scripts, fpm-fcgi for PHP-FPM, apache2handler for mod_php, and embedded SAPIs like FrankenPHP's. PHP_SAPI tells you which one you are in.

The classic model is shared-nothing: for every request PHP initialises the request state, compiles (or fetches from OPcache) and runs the script, sends the output and then frees all request memory. Nothing leaks between requests, so a bug can't corrupt the next user's state, and a crash affects one request.

PHP-FPM runs a master process and a pool of worker processes; each worker handles one request at a time. The web server (nginx, Caddy) forwards requests over FastCGI. Key pool settings:

  • pm = dynamic | static | ondemand and pm.max_children: the maximum concurrent requests. Size it as available RAM divided by memory per worker.
  • pm.max_requests recycles workers to contain slow memory leaks.
  • request_terminate_timeout and max_execution_time cap runaway scripts, and memory_limit caps memory per request.

Because a worker blocks while waiting on a database or API, slow I/O directly reduces throughput. The status page (pm.status_path) and slow log (request_slowlog_timeout) show where time goes.

Worker modes (FrankenPHP workers, RoadRunner, Swoole, Laravel Octane) boot the app once and keep it in memory across requests. They are much faster, but give up shared-nothing: static properties, singletons and globals now persist, and must be reset per request.

Example

; /etc/php/8.4/fpm/pool.d/www.conf (excerpt)
[www]
listen = /run/php/php8.4-fpm.sock
pm = dynamic
; ~ (RAM for PHP) / (avg worker RSS), e.g. 4 GB / 80 MB = 50
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
; recycle workers to contain leaks
pm.max_requests = 1000
request_terminate_timeout = 30s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log
pm.status_path = /fpm-status
php_admin_value[memory_limit] = 256M

Remember

  • PHP runs behind a SAPI; FPM uses a pool of single-request worker processes
  • pm.max_children bounds concurrency; size it from memory per worker
  • Worker modes boot once and trade shared-nothing safety for speed

Watch out for

  • Raising pm.max_children beyond available RAM, causing swapping or OOM kills under load.

Check yourself

  1. With pm.max_children = 20 and every request waiting 1 s on a slow API, roughly how many requests per second can the pool serve?
  2. What changes when an app runs in a worker mode like FrankenPHP or Octane?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.php.net/manual/en/install.fpm.php