The real bottleneck nobody talks about.
Most developers never touch PHP-FPM configuration. They keep the defaults (designed for development, not production). When traffic spikes, PHP-FPM either spawns too many processes (memory exhaustion) or too few (request queueing). Getting these settings right is critical for production.
PHP-FPM workers are persistent processes. They don't die after each request (unlike CGI). This is good for performance, but bad for memory leaks — a leaking worker stays alive for thousands of requests, accumulating memory until it crashes.
| Setting | What It Does | Default | Production Value |
|---|---|---|---|
pm
| Process manager type | dynamic
| dynamic or ondemand
|
pm.max_children
| Maximum number of worker processes | 5-10 (varies) | Calculated based on RAM |
pm.start_servers
| Number of workers created on startup | 2-4 (varies) | max_children / 4
|
pm.min_spare_servers
| Minimum idle workers (dynamic mode) | 1-2 (varies) | start_servers
|
pm.max_spare_servers
| Maximum idle workers (dynamic mode) | 3-6 (varies) | max_children / 2
|
pm.max_requests
| Requests per worker before restart | 0 (unlimited) | 500-1000 |
request_terminate_timeout
| Max execution time per request | 0 (no limit) | 30-60 seconds |
Setting pm.max_children too high → Server runs out of memory, crashes, OOM killer kills processes randomly.
Setting pm.max_children too low → Requests queue up, users see 502 errors, response times skyrocket.
# Check memory usage of PHP-FPM processes
ps aux | grep "php-fpm" | awk '{sum+=$6} END {print sum/NR/1024 " MB"}'
# Or with more detail
watch -n 1 'ps aux | grep "php-fpm" | head -20'
| Mode | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
dynamic
| Maintains min/max spare idle workers | Fast response (workers ready) | Consumes memory even when idle | High-traffic, consistent load |
ondemand
| Creates workers only when needed, kills after idle time Saves memory during low traffic | Cold start delay on first request | Low-traffic, variable load, cron-heavy apps | |
static
| Fixed number of workers (max_children) | Predictable, no spawning overhead | Wastes memory if traffic is low | Very high-traffic, predictable load |
# dynamic mode (recommended for most Laravel apps)
pm = dynamic
pm.max_children = 100
pm.start_servers = 25
pm.min_spare_servers = 25
pm.max_spare_servers = 50
pm.max_requests = 500
# ondemand mode (memory saving)
pm = ondemand
pm.max_children = 100
pm.process_idle_timeout = 10s
pm.max_requests = 500
# static mode (predictable, no spawning)
pm = static
pm.max_children = 100
pm.max_requests = 500
Always set pm.max_requests to 500-1000. This is the single most important PHP-FPM setting for production stability. It prevents memory leaks from killing your server.
A single slow request (e.g., external API timeout, database deadlock) can hang a worker forever. That worker never processes another request. Over time, all workers hang. Server stops responding to ANY request.
# php-fpm.conf
request_terminate_timeout = 60s
# If a request takes longer than 60 seconds, PHP-FPM kills the worker
# A new worker is spawned immediately
# Your server keeps running
max_execution_time in php.ini is a soft limit (PHP can ignore in some cases).
request_terminate_timeout in php-fpm.conf is a hard kill — the process is terminated by the OS.
# php-fpm.conf
pm.status_path = /status
# Nginx config
location ~ ^/status$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
curl http://localhost/status
pool: www
process manager: dynamic
start time: 1704067200
start since: 3600
accepted conn: 12500
listen queue: 0 ← If this grows, you need more workers
max listen queue: 50
listen queue len: 128
idle processes: 15
active processes: 10 ← Currently handling requests
total processes: 25
max active processes: 45 ← Peak concurrent requests
max children reached: 0 ← If >0, increase max_children
max_childrenmax_children is too low; /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
; Process manager settings
pm = dynamic
pm.max_children = 100
pm.start_servers = 25
pm.min_spare_servers = 25
pm.max_spare_servers = 50
pm.max_requests = 500
; Request timeout
request_terminate_timeout = 60s
request_slowlog_timeout = 5s
slowlog = /var/log/php8.2-fpm-slow.log
; Status page
pm.status_path = /status
ping.path = /ping
ping.response = pong
; Security
; Prevent users from seeing environment variables
clear_env = no
; Child processes
catch_workers_output = yes
decorate_workers_output = no
| Setting | Default (Dev) | Production | Why |
|---|---|---|---|
| pm | dynamic | dynamic or ondemand | Depends on traffic pattern |
| pm.max_children | 5-10 | (RAM - other) / 40MB | Prevents OOM and queueing |
| pm.max_requests | 0 | 500-1000 | Prevents memory leaks |
| request_terminate_timeout | 0 | 60s | Kills hung requests |
Topic 17: OpCache & PHP JIT — PHP's built-in performance superpowers. Why OpCache is mandatory for production. How JIT can make CPU-intensive code 3-5x faster.