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 | ondemandandpm.max_children: the maximum concurrent requests. Size it as available RAM divided by memory per worker.pm.max_requestsrecycles workers to contain slow memory leaks.request_terminate_timeoutandmax_execution_timecap runaway scripts, andmemory_limitcaps 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] = 256MRemember
- 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.