PHP-FPM Process Manager Tuning on Linux
AI generated
$
/etc
Linux · PHP-FPM · Magento · Performance
PHP-FPM Process Manager Tuning on Linux
Calculate pm.max_children instead of guessing

The PHP-FPM process manager decides how many parallel requests a Magento server can handle before requests start queuing up or too many workers exhaust available memory. Setting pm.max_children by gut feeling risks either wasted capacity or an out of memory crash under load.

18 min read pm.max_children · static · dynamic · ondemand PHP-FPM 8.x · Linux · Magento 2

1. Why the PHP-FPM process manager decides server capacity

The PHP-FPM process manager is the component that decides how many PHP worker processes are allowed to run in parallel on a Linux server. Every incoming HTTP request to Magento is handled by exactly one worker process, and as long as that process stays busy it cannot accept a second request. If the number of workers is insufficient, requests pile up in the backlog of the Unix socket or TCP port until a worker becomes free or the client hits a timeout.

In practice, many servers run either with the distribution defaults or with values that were generously guessed once and never revisited. Both approaches lead to problems. Too few workers slow the checkout down under load, too many workers compete for the same memory and provoke the OOM killer. The PHP-FPM process manager therefore needs to be configured based on real measurements, not assumptions.

This article shows how to calculate the right number of worker processes for a Magento server on Linux, which of the three operating modes fits which load profile, and how to continuously validate the configuration using the built in status page and slowlog instead of setting it once and forgetting about it.

2. The three operating modes: static, dynamic, ondemand

The PHP-FPM process manager supports three modes, selected via the pm directive in the pool. In static mode, PHP-FPM immediately starts exactly pm.max_children worker processes and keeps that count constant regardless of current load. This delivers the most predictable memory usage because the maximum process count is fixed from the start and never changes during operation.

In dynamic mode, PHP-FPM starts with pm.start_servers worker processes and then scales between pm.min_spare_servers and pm.max_spare_servers depending on load, up to a maximum of pm.max_children. This mode adapts to fluctuating load and consumes less memory during quiet periods, but it introduces a brief delay during sudden load spikes because new worker processes must be started before they can accept requests.

The third mode, ondemand, starts no workers at all when idle. A process is only created when a request arrives, and it is terminated again after pm.process_idle_timeout seconds without a new request. This mode suits staging environments or rarely used secondary domains, but it is the wrong choice for a production Magento storefront with constant traffic, because every new process start costs time that becomes noticeable on a heavily visited shop.


; /etc/php/8.3/fpm/pool.d/magento.conf
; PHP-FPM Process Manager mode comparison — same pool, three variants

; Variant A: static — fixed worker count, most predictable memory usage
pm = static
pm.max_children = 40

; Variant B: dynamic — scales between min and max based on load
pm = dynamic
pm.max_children = 40
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 20

; Variant C: ondemand — no idle workers, spawns on first request
pm = ondemand
pm.max_children = 40
pm.process_idle_timeout = 30s

3. The formula for pm.max_children

The central mistake in PHP-FPM process manager tuning is setting pm.max_children without any relation to available memory. The correct formula is: memory available for PHP-FPM, divided by the average memory consumption of a single worker process under real load. The available memory here is not the entire server memory, but the portion left over after MySQL, Redis, Nginx, Elasticsearch and the operating system itself have taken their share.

The most reliable way to measure average memory consumption per worker is ps --sort=-rss -o pid,rss,cmd -C php-fpm8.3 during a realistic load phase, not right after restarting the service when the PHP opcode cache is still cold. A Magento 2 worker with a warm opcache and an active object manager often sits between 90 and 180 megabytes RSS, depending on installed modules and the PHP version in use. Measuring this value once and revalidating it annually leads to far more reliable decisions than copying a generic value from a forum post.

An example: a server with 16 GB of RAM, of which MySQL uses 6 GB, Redis 1 GB and the operating system plus Nginx roughly 1 GB, has 8 GB left for PHP-FPM. At an average of 130 MB per worker, that works out to 8000 MB divided by 130 MB, roughly 61 possible workers. In practice, a safety margin of 15 to 20 percent is subtracted to absorb load spikes with larger requests, which brings pm.max_children down to a value around 48 to 50.


#!/usr/bin/env bash
# measure-fpm-memory.sh — average RSS per PHP-FPM worker under real load
set -euo pipefail

FPM_PROCESS_NAME="php-fpm8.3"

# Sum RSS of all worker processes (excludes the master process by name filter)
total_kb=$(ps --no-headers -o rss -C "$FPM_PROCESS_NAME" | awk '{sum+=$1} END {print sum}')
worker_count=$(pgrep -c -f "$FPM_PROCESS_NAME: pool")

if [[ -z "$worker_count" || "$worker_count" -eq 0 ]]; then
  echo "[ERROR] No active workers found for $FPM_PROCESS_NAME" >&2
  exit 1
fi

avg_mb=$(( total_kb / worker_count / 1024 ))
echo "Active workers: $worker_count"
echo "Average RSS per worker: ${avg_mb} MB"
echo "Suggested pm.max_children for 8000 MB budget: $(( 8000 / avg_mb ))"

4. A complete pool configuration for Magento

Beyond pm and pm.max_children, a production PHP-FPM process manager needs additional fine tuning that is often overlooked. The pm.max_requests directive terminates a worker after a fixed number of processed requests and restarts it, limiting memory leaks caused by extensions or faulty modules without any administrator intervention. For Magento, a value between 500 and 1000 has proven effective, depending on how memory intensive the installed third party modules are.

Separate pools for different use cases are another important pattern. The frontend pool serving storefront traffic should be separated from a dedicated pool for cron jobs and indexer processes, because long running indexer tasks would otherwise block worker capacity intended for customer requests. Separate Unix sockets per pool allow targeted routing in Nginx and separate metrics per pool in monitoring.


; /etc/php/8.3/fpm/pool.d/magento-frontend.conf
[magento-frontend]
user = magento
group = magento
listen = /run/php/magento-frontend.sock
listen.owner = www-data
listen.group = www-data

pm = static
pm.max_children = 48
pm.max_requests = 800

; Do not let a single slow request block indefinitely
request_terminate_timeout = 60s

pm.status_path = /fpm-status-frontend
ping.path = /fpm-ping

php_admin_value[memory_limit] = 756M
php_admin_value[opcache.max_accelerated_files] = 60000

5. The status page as live diagnostics

The built in pm.status_path is one of the least used tools in the PHP-FPM process manager. Once enabled, it delivers in real time the number of active and idle workers, the total number of requests handled so far, the number of requests that had to wait for a free worker, and the timestamp of the last restart. These exact values show whether pm.max_children is set too low, long before customers notice any timeouts.

The single most important value on the status page is listen queue. If this number is regularly above zero, it means requests are waiting because all configured workers are already busy. A persistently filled listen queue is the most reliable early warning signal that pm.max_children needs to be increased, even before users perceive any delay at all.


#!/usr/bin/env bash
# check-fpm-status.sh — poll the FPM status page and alert on queue backlog
set -euo pipefail

STATUS_URL="http://127.0.0.1/fpm-status-frontend?json"
MAX_QUEUE=5

response=$(curl -fsS "$STATUS_URL")
listen_queue=$(echo "$response" | jq -r '."listen queue"')
active=$(echo "$response" | jq -r '."active processes"')
idle=$(echo "$response" | jq -r '."idle processes"')

echo "Active: $active | Idle: $idle | Listen queue: $listen_queue"

if (( listen_queue > MAX_QUEUE )); then
  echo "[WARN] Listen queue exceeds threshold — consider raising pm.max_children" >&2
  exit 1
fi

6. Slowlog: tracking down slow requests

Besides the number of workers, the duration of individual requests also affects the capacity of the PHP-FPM process manager. A worker that spends 8 seconds on a slow database query blocks a slot for that entire time, a slot that could otherwise have served several fast requests. The request_slowlog_timeout directive together with slowlog writes a full PHP backtrace to a separate log file whenever a request exceeds the configured threshold.

This backtrace shows exactly which method and line the code is stuck in when a request is flagged as slow, without requiring a profiler such as Xdebug to be enabled in production. In practice, the slowlog frequently reveals uncached price rules, missing database indexes or external API calls without a timeout, issues that barely register in normal operation but can block entire worker pools under load.


; /etc/php/8.3/fpm/pool.d/magento-frontend.conf (excerpt)
; Log full PHP backtrace for any request slower than 5 seconds
request_slowlog_timeout = 5s
slowlog = /var/log/php/magento-frontend-slow.log

; Read a captured trace with:
; tail -n 40 /var/log/php/magento-frontend-slow.log

7. systemd, limits and the PHP-FPM service

The PHP-FPM process manager runs on Linux as a systemd service, and its unit file sets its own resource limits that apply independently of the pool configuration. The LimitNOFILE directive in the systemd unit limits the maximum number of open file descriptors per process, and a value set too low leads to cryptic too many open files errors under many concurrent database and cache connections, errors that at first glance seem unrelated to the process manager at all.

A second frequently overlooked point: systemctl reload php8.3-fpm reloads the configuration without abruptly terminating existing workers, while systemctl restart immediately drops all connections. For changes to the pool configuration in a running system, reload is almost always the right choice, because active requests are allowed to finish before the old master process is replaced by the new one.


# /etc/systemd/system/php8.3-fpm.service.d/override.conf
# systemctl edit php8.3-fpm — creates this drop-in file

[Service]
LimitNOFILE=65536
OOMScoreAdjust=-500

# Apply changes:
# systemctl daemon-reload
# systemctl restart php8.3-fpm

# Verify the effective limit for a running worker:
# cat /proc/$(pgrep -f "php-fpm8.3: pool magento-frontend" | head -1)/limits | grep "open files"

8. Common mistakes in process manager tuning

The most common mistake is setting pm.max_children far too high, assuming more workers can never hurt. The opposite is true: when the sum of all worker processes needs more memory than is physically available, the Linux kernel resorts to swapping or the OOM killer terminates processes at random, and it often does not even hit the PHP-FPM worker itself but rather MySQL or Redis. A value set too high is therefore not a safe buffer but a latent failure risk for the entire server.

A second mistake is running dynamic mode with a very low pm.min_spare_servers and expecting PHP-FPM to absorb load spikes without any noticeable delay. Every newly started worker process has to initialize PHP, attach to the opcache and load autoloader classes, which on a production Magento server can easily take 100 to 300 milliseconds. Anyone with regular, predictable load spikes, for example due to scheduled marketing campaigns, runs noticeably more stably with static and a generously sized fixed worker count.

9. The three modes compared directly

Choosing the right mode for the PHP-FPM process manager depends directly on the server's load profile. The table below summarizes when each mode is the better choice.

Mode Idle memory usage Reaction to load spikes Recommendation
static Constant and high, predictable Immediate, no startup delay Production Magento storefront
dynamic Low to medium, fluctuating Brief delay while scaling up Cron pools, variable load
ondemand Minimal, no idle workers Noticeable delay on cold start Staging, rarely used secondary domains

For most production Magento installations with consistently high traffic, static is the more robust choice, because the PHP-FPM process manager does not lose time starting new processes. A hybrid approach with separate pools, static for the frontend storefront and ondemand for rarely accessed backend tools, combines the benefits of both modes without compromising storefront performance.

Mironsoft

Linux server tuning and performance optimization for Magento

Is PHP-FPM configured correctly, or just guessed?

We analyze your existing PHP-FPM process manager configuration, calculate the right worker count based on real memory measurements, and set up monitoring for the status page and slowlog.

Capacity analysis

Measure memory usage per worker and calculate pm.max_children correctly

Pool separation

Cleanly configure separate pools for storefront, cron and indexer

Monitoring

Integrate the status page and slowlog into existing monitoring

10. Summary

The PHP-FPM process manager is not a one time setting but an ongoing part of server capacity planning. The formula of available RAM divided by measured memory consumption per worker delivers a solid starting value for pm.max_children, instead of adopting an estimated number from a forum post. The static mode fits best for most production Magento storefronts, because it introduces no delay from newly starting worker processes.

The status page and slowlog provide the measurement data needed to continuously validate the configuration instead of setting it just once. A filled listen queue shows that more workers are needed, a full slowlog shows exactly which code paths block worker capacity for too long. Anyone who integrates both signals into existing monitoring detects capacity problems before customers experience timeouts at checkout.

PHP-FPM Process Manager Tuning — The Essentials at a Glance

Formula

Available RAM divided by measured RSS per worker, minus a 15 to 20 percent safety margin.

Mode choice

static for consistently high traffic, ondemand only for rarely used secondary domains.

Monitoring

Enable pm.status_path and slowlog, monitor the listen queue as an early warning signal.

Operations

Use systemctl reload instead of restart for configuration changes in a running system.

11. FAQ: PHP-FPM Process Manager Tuning on Linux

1What exactly does the PHP-FPM process manager do?
It manages how many PHP worker processes run in parallel and how they are started, scaled or terminated depending on the mode.
2How do I calculate pm.max_children correctly?
Divide available RAM by the measured RSS per worker, then subtract a 15 to 20 percent safety margin.
3Which mode is best suited for Magento?
Static for consistently high traffic, dynamic for cron pools, ondemand only for rarely used secondary domains.
4What does the pm.status_path page show?
Active and idle workers, request counts and above all the listen queue as an early warning signal.
5What is the slowlog used for?
Writes a full PHP backtrace for slow requests without requiring a profiler to be enabled in production.
6Why separate pools for frontend and cron?
Long running cron and indexer processes would otherwise block workers intended for customer requests.
7What happens when pm.max_children is too high?
Too many workers can trigger swapping or the OOM killer, often hitting MySQL or Redis instead of PHP-FPM.
8What does pm.max_requests do?
Automatically restarts a worker after a fixed number of requests, limiting memory leaks.
9Reload or restart for configuration changes?
Reload lets active requests finish, restart drops them immediately. For a running system, reload is preferred.
10How does LimitNOFILE relate to this?
systemd limits open file descriptors per process. A value set too low causes too many open files errors.