PHP-FPM
What Is PHP-FPM?
In simple terms: A web server such as Nginx is excellent at handling connections and serving files, but it cannot run PHP itself. PHP-FPM is a separate service that keeps a group of PHP worker processes ready. The web server passes each PHP request to PHP-FPM, a free worker runs the script, and the result goes back to the web server and on to the browser. Technically: PHP-FPM (FastCGI Process Manager) is PHP's production SAPI for serving web requests over the FastCGI protocol. A master process reads the configuration and manages one or more pools of worker processes. Each worker handles one request at a time, so the number of workers determines how many PHP requests can run concurrently. Pools can run as different users with different settings, isolating applications from each other.
Browser ──HTTPS──► Nginx ──FastCGI (Unix socket)──► PHP-FPM master
│ ├── worker 1 (running a request)
└── serves static files directly ├── worker 2 (idle)
└── worker 3 (running a request)
Installing and Running PHP-FPM
# Debian / Ubuntu
sudo apt install php8.4-fpm
sudo systemctl enable --now php8.4-fpm
systemctl status php8.4-fpm
| Command | Effect |
|---|---|
sudo systemctl reload php8.4-fpm | Graceful reload: re-reads configuration, replaces workers after their current requests finish, clears OPcache |
sudo systemctl restart php8.4-fpm | Full restart (brief interruption) |
sudo php-fpm8.4 -t | Test the configuration for errors before reloading |
Configuration files on Debian/Ubuntu:
| File | Contents |
|---|---|
/etc/php/8.4/fpm/php-fpm.conf | Global settings (logging, includes) |
/etc/php/8.4/fpm/pool.d/www.conf | The default pool |
/etc/php/8.4/fpm/php.ini | php.ini used by FPM (separate from the CLI's) |
Connecting Nginx to PHP-FPM
server {
listen 443 ssl;
server_name shop.example;
root /var/www/shop/current/public;
index index.php;
location / {
try_files $uri /index.php$is_args$args;
}
location = /index.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root/index.php;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
location ~ \.php$ {
return 404;
}
}
- Requests for existing files (CSS, images) are served by Nginx directly; everything else goes to the front controller (see Hiding PHP).
fastcgi_passpoints to the pool's Unix socket (faster than TCP when Nginx and PHP-FPM run on the same machine; use127.0.0.1:9000or a container name with TCP).$realpath_rootresolves symlinks, so deployments that switch acurrentsymlink pick up the new release reliably.
Configuring a Pool
; /etc/php/8.4/fpm/pool.d/shop.conf
[shop]
user = shop
group = shop
listen = /run/php/shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 40
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 1000
request_terminate_timeout = 60s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/shop-slow.log
pm.status_path = /fpm-status
ping.path = /fpm-ping
php_admin_value[memory_limit] = 256M
php_admin_value[error_log] = /var/log/php-fpm/shop-error.log
php_admin_flag[log_errors] = on
Key Settings
| Setting | Meaning |
|---|---|
user / group | The account the workers run as — a dedicated user per application isolates sites from each other |
listen | Socket (or address) the web server connects to |
pm | Process manager mode: dynamic, static, or ondemand |
pm.max_children | Maximum number of workers = maximum concurrent PHP requests |
pm.start_servers, pm.min_spare_servers, pm.max_spare_servers | Initial and idle worker counts (dynamic mode) |
pm.max_requests | Recycle a worker after this many requests — limits the impact of memory leaks |
request_terminate_timeout | Kill requests that run too long |
request_slowlog_timeout + slowlog | Log a stack trace for requests slower than the threshold — great for finding slow code |
pm.status_path, ping.path | Monitoring endpoints (restrict access to them in Nginx) |
php_admin_value[...] | php.ini settings for this pool that scripts cannot override |
Process Manager Modes
| Mode | Behavior | Best For |
|---|---|---|
dynamic | Keeps a range of idle workers, grows up to max_children | Most servers |
static | Always runs exactly max_children workers | Dedicated servers with steady, high traffic |
ondemand | Starts workers only when requests arrive, stops idle ones | Low-traffic sites, many small pools on one server |
Sizing pm.max_children
Each worker uses memory. If too many run at once, the server starts swapping or the out-of-memory killer terminates processes. A simple estimate:
<?php
function maxChildren(int $totalMemoryMb, int $reservedMb, int $averageWorkerMb): int
{
return max(1, intdiv($totalMemoryMb - $reservedMb, $averageWorkerMb));
}
// 4 GB server, 1 GB reserved for the OS, MySQL, Redis; workers average 60 MB
echo maxChildren(4096, 1024, 60) . "\n";
Output
51
- Measure the average worker memory under real load (for example,
ps -o rss= -C php-fpm8.4and average the values, or monitoring tools). - Subtract memory needed by the operating system and other services.
- Divide the rest by the average worker size; round down and leave a safety margin. If the log reports
server reached pm.max_children setting, requests are waiting for free workers. Either the limit is too low for the traffic — or, more often, requests are slow (database queries, external APIs) and occupy workers for too long. Fix slow requests first.
Monitoring With the Status Page
With pm.status_path enabled and access restricted to internal addresses, the status page shows the pool's state:
pool: shop
process manager: dynamic
start time: 17/Oct/2026:09:00:00 +0530
start since: 3600
accepted conn: 15234
listen queue: 0
max listen queue: 3
idle processes: 6
active processes: 2
total processes: 8
max active processes: 14
max children reached: 0
slow requests: 1
- listen queue greater than 0 means requests are waiting for a worker.
- max children reached counts how often the pool hit its limit.
- slow requests counts entries written to the slow log.
Finishing the Response Early: fastcgi_finish_request()
PHP-FPM offers a function that sends the response to the browser and then continues running the script:
<?php
echo json_encode(['status' => 'Order received']);
fastcgi_finish_request(); // the user gets the response now
// Continue working without making the user wait
sendConfirmationEmail($orderId);
This is convenient for short follow-up tasks, but the worker remains busy until the script ends. For anything slow or important, use a real queue with background workers.
Troubleshooting
| Symptom | Likely Cause |
|---|---|
502 Bad Gateway | PHP-FPM not running, wrong socket path, or socket permissions (listen.owner/listen.mode) |
504 Gateway Timeout | Script exceeded the web server's fastcgi_read_timeout (or FPM's terminate timeout) |
| Slow responses under load | Workers exhausted (max_children reached), slow queries, external API calls |
Changes to php.ini have no effect | The CLI's file was edited, or FPM was not reloaded |
| Memory growing per worker | Leaks in long requests — lower pm.max_requests and investigate |
Check the FPM log (/var/log/php8.4-fpm.log), the pool's error and slow logs, and the Nginx error log.
Common Mistakes
Setting pm.max_children Too High
Effect: The server runs out of memory under load. Fix: Calculate from measured worker memory and available RAM.
Running All Sites in One Pool as One User
Effect: A compromised site can read the others' files. Fix: One pool and user per application.
Exposing the Status Page
Effect: Internal information is visible to everyone. Fix: Restrict status and ping paths to internal IPs or authentication.
Not Reloading After Configuration Changes
Effect: Old settings remain active. Fix: Test with php-fpm8.4 -t, then reload.
Best Practices
- Run PHP behind Nginx (or another server) with PHP-FPM, using Unix sockets on the same host.
- Create a pool with its own user per application, and enforce limits with
php_admin_value. - Size
pm.max_childrenfrom measured memory, and recycle workers withpm.max_requests. - Enable the slow log and status page (restricted) to monitor performance.
- Move slow work to queues instead of tying up workers. Official reference: https://www.php.net/manual/en/install.fpm.php