Magento 2 PHP-FPM Tuning: Process Manager, Pool Configuration & Performance

php dev.to

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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" }'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 3 — Divide

13184 / 120 ≈ 109
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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() ...
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Enable in pool config:

pm.status_path = /fpm-status
Enter fullscreen mode Exit fullscreen mode

Then query:

curl http://127.0.0.1/fpm-status?full
Enter fullscreen mode Exit fullscreen mode

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 = dynamic for production Magento
  • [ ] Calculate pm.max_children from real RSS measurements, not guesses
  • [ ] Keep memory_limit ≤ 768M for web pools; use a separate high-memory pool for CLI
  • [ ] Enable request_terminate_timeout = 60s to kill runaway workers
  • [ ] Set pm.max_requests = 500 to periodically recycle workers
  • [ ] Enable the slow-log in staging/production for regression detection
  • [ ] Expose /fpm-status internally and monitor queue length + active workers
  • [ ] Create a separate admin pool with lower pm.max_children and 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'"
Enter fullscreen mode Exit fullscreen mode

Use ab (Apache Bench) or wrk for a quick load test:

wrk -t4 -c50 -d30s https://yourstore.com/
Enter fullscreen mode Exit fullscreen mode

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.

Source: dev.to

arrow_back Back to Tutorials