PHP-FPM is the engine room of every Magento 2 store. Get it wrong and your perfectly-optimised Nginx, Redis, and OPcache setup still falls over under load. Get it right and you'll handle traffic spikes cleanly without throwing 502 errors or burning CPU on context-switching between hundreds of idle workers.
This guide covers everything from choosing the right process manager to calculating the optimal pm.max_children, sizing memory limits, enabling the slow-log, and verifying your changes actually stick.
Why PHP-FPM Configuration Matters for Magento
Magento is memory-hungry. A single PHP worker handling a catalog page, a checkout step, or an API call typically consumes 80–200 MB of RAM. Multiply that by every concurrent request and you quickly exceed available memory—at which point Linux starts swapping, latency explodes, and your monitoring goes red.
The PHP-FPM process manager sits between Nginx and PHP itself. It decides:
- How many PHP processes run simultaneously
- Whether new processes are spawned on demand or kept alive in a warm pool
- How long an idle worker waits before being killed
- How requests are queued when all workers are busy
Every one of those decisions directly affects Magento's response time and resource usage.
Choosing the Right Process Manager (pm)
PHP-FPM ships with three process manager modes: static, dynamic, and ondemand.
pm = static
All workers are started at boot and kept alive forever. Memory is reserved upfront—no warm-up delay for the first request, no forking overhead under load.
Best for: dedicated servers with predictable, continuous traffic. The worker count is fixed, so you need enough RAM to sustain it always.
pm = static
pm.max_children = 20
pm = dynamic
Workers are started on demand up to pm.max_children, but a minimum pool (pm.min_spare_servers) is kept alive so sudden bursts don't start from zero.
Best for: Magento stores with variable traffic—daytime peak, overnight lull. This is the most common production setting.
pm = dynamic
pm.max_children = 30
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500
pm = ondemand
Workers are created for each request and killed after pm.process_idle_timeout seconds of inactivity. Zero memory cost when idle, but cold-start latency on the first request after a quiet period.
Best for: dev environments, staging, or low-traffic stores where saving RAM matters more than latency.
Calculating pm.max_children
This is the most important setting. Too low → 502 errors under load. Too high → OOM kills and swap thrashing.
The formula:
pm.max_children = (Available RAM) / (Average PHP worker RSS)
Step 1 — Measure real Magento worker RSS
ps --no-headers -o rss,comm -C php-fpm | awk '{ sum += $1 } END { print sum/NR/1024 " MB avg" }'
For a typical Magento 2 store with OPcache enabled:
- Storefront page: 80–120 MB
- Checkout / payment: 120–160 MB
- Admin panel: 150–200 MB
- GraphQL / REST API: 100–140 MB
Use the 90th-percentile figure from production, not the minimum. For most Magento stores, 110–130 MB is a safe baseline.
Step 2 — Determine available RAM
Available RAM = Total RAM − OS overhead − MySQL / Redis / Varnish / Nginx memory
On a 16 GB server running only Nginx + PHP-FPM + Redis:
16384 MB
- 512 MB OS + system
- 2048 MB MySQL (innodb_buffer_pool_size + connections)
- 512 MB Redis
- 128 MB Nginx
= 13184 MB available for PHP-FPM
Step 3 — Divide
13184 / 120 ≈ 109
Round down with a 10–15 % safety margin: pm.max_children = 90
Leave headroom for memory spikes (large imports, complex cart rules, heavy admin operations).
Full Production Pool Configuration
; /etc/php/8.3/fpm/pool.d/magento.conf
[magento]
user = www-data
group = www-data
; Socket is faster than TCP for local Nginx
listen = /run/php/php8.3-fpm-magento.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; Process manager
pm = dynamic
pm.max_children = 60
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
; Recycle workers after N requests to prevent memory leaks
pm.max_requests = 500
; Kill a worker that has been processing for more than 60 s
request_terminate_timeout = 60s
; Slow-log: capture stack traces for requests taking > 3 s
slowlog = /var/log/php-fpm/magento-slow.log
request_slowlog_timeout = 3s
; PHP settings override per pool
php_admin_value[memory_limit] = 768M
php_admin_value[max_execution_time] = 600
php_admin_value[error_log] = /var/log/php-fpm/magento-error.log
php_flag[display_errors] = off
; Environment variables
env[MAGE_MODE] = production
env[MAGE_RUN_TYPE] = store
Memory Limit: 768M vs 2G
You'll see memory_limit = 2G in many Magento "hardening" guides. That's a cargo-cult setting borrowed from import scripts. For storefront requests:
| Request type | Realistic limit |
|---|---|
| Storefront page | 256M |
| Checkout / order | 512M |
| Admin panel general | 512M |
| CLI / imports | 2G–4G (separate pool or CLI ini) |
Set memory_limit = 768M in your web pool and override with php_admin_value[memory_limit] = 4G in a dedicated CLI pool or your php.ini for php-cli. This prevents a single rogue storefront worker from consuming 2 GB while leaving other workers starved.
Separate Admin Pool (Optional but Recommended)
Admin users run heavier operations—mass attribute updates, report generation, catalogue imports, cache flushes. Give them their own pool so they never crowd out storefront workers:
; /etc/php/8.3/fpm/pool.d/magento-admin.conf
[magento-admin]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm-magento-admin.sock
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
pm.max_requests = 100
php_admin_value[memory_limit] = 1G
php_admin_value[max_execution_time] = 900
In Nginx, route /admin/ and /index.php/admin/ to the admin socket and everything else to the storefront socket.
location ~ ^/index\.php(/|$) {
set $fpm_socket /run/php/php8.3-fpm-magento.sock;
if ($request_uri ~* "^/admin") {
set $fpm_socket /run/php/php8.3-fpm-magento-admin.sock;
}
fastcgi_pass unix:$fpm_socket;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
Using the Slow-Log to Find Magento Bottlenecks
The PHP-FPM slow-log captures a full PHP stack trace for every request that exceeds request_slowlog_timeout. It is one of the fastest ways to spot Magento performance regressions without attaching a profiler.
Enable it (see pool config above), reproduce the slow request, then inspect:
tail -f /var/log/php-fpm/magento-slow.log
Sample output:
[02-Jun-2026 09:14:32] [pool magento] pid 12345
script_filename = /var/www/magento/index.php
[0x00007f...] catalogProductRepository->get() /vendor/magento/module-catalog/Model/ProductRepository.php:208
[0x00007f...] \Magento\Catalog\Model\Layer\Category\ItemCollectionProvider->getCollection() ...
[0x00007f...] \Magento\LayeredNavigation\Block\Navigation::_prepareLayout() ...
The slow-log revealed layered navigation collection loading—a known hot path covered in our layered navigation performance guide.
Monitoring Pool Status
PHP-FPM exposes a /status endpoint showing active/idle workers, request queue length, and total requests served:
location ~ ^/fpm-status$ {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.3-fpm-magento.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Enable in pool config:
pm.status_path = /fpm-status
Then query:
curl http://127.0.0.1/fpm-status?full
Key metrics to watch:
| Metric | Warning threshold |
|---|---|
listen queue |
> 0 for sustained periods → increase pm.max_children
|
active processes |
Consistently at pm.max_children → workers exhausted |
max active processes |
Near pm.max_children → approaching limit |
slow requests |
Growing fast → investigate slow-log |
Plug /fpm-status into Prometheus (via php-fpm_exporter), Datadog, or New Relic for time-series alerting.
pm.max_requests: Preventing Memory Leaks
Magento 2 and many third-party modules are not perfectly clean: objects accumulate in memory across requests, registry entries linger, and some custom code leaks closures. Setting pm.max_requests = 500 instructs PHP-FPM to gracefully restart each worker after 500 handled requests, clearing all accumulated memory.
The trade-off is a tiny warm-up cost (OPcache re-fill for the new worker) which is negligible at max_requests = 500. For stores with lots of third-party extensions or known leak issues, drop this to 200–300.
Quick Reference Checklist
- [ ] Use
pm = dynamicfor production Magento - [ ] Calculate
pm.max_childrenfrom real RSS measurements, not guesses - [ ] Keep
memory_limit≤ 768M for web pools; use a separate high-memory pool for CLI - [ ] Enable
request_terminate_timeout = 60sto kill runaway workers - [ ] Set
pm.max_requests = 500to periodically recycle workers - [ ] Enable the slow-log in staging/production for regression detection
- [ ] Expose
/fpm-statusinternally and monitor queue length + active workers - [ ] Create a separate admin pool with lower
pm.max_childrenand higher memory/time limits - [ ] Prefer Unix sockets over TCP for Nginx ↔ PHP-FPM on the same host
Verifying Configuration
After editing pool files, always validate before reloading:
php-fpm8.3 -t # syntax check
systemctl reload php8.3-fpm
# Confirm workers started
ps aux | grep php-fpm | wc -l
# Watch status under a load test
watch -n1 "curl -s http://127.0.0.1/fpm-status | grep -E 'active|idle|queue'"
Use ab (Apache Bench) or wrk for a quick load test:
wrk -t4 -c50 -d30s https://yourstore.com/
Watch the FPM status during the test. If listen queue climbs above 0, you need more workers (more RAM) or faster PHP execution (profiling, caching, fewer plugins).
PHP-FPM tuning is not a one-and-done task. As your Magento store grows—more SKUs, more extensions, higher traffic—revisit pm.max_children quarterly and after major upgrades. The fifteen minutes it takes to recalculate and re-tune pays for itself the first time you dodge a traffic-spike outage.