← Back to blog

Magento 2 PHP-FPM Tuning: Workers, OPcache, php.ini

Rodrigo Biazzi

PHP-FPM is where Magento 2 spends most of its time on an uncached request. Get the pool size wrong and the server either sits idle while visitors wait in a queue, or it runs out of memory and starts killing processes. Get OPcache wrong and every request compiles thousands of PHP files from scratch. This guide covers PHP-FPM pool settings, OPcache and the php.ini values that matter for Magento Open Source and Adobe Commerce 2.4.8 on PHP 8.3 and 8.4.

How a request uses PHP-FPM

Nginx passes each PHP request to a PHP-FPM pool over a socket. The pool has a set of worker processes. Each worker handles one request at a time. If all workers are busy, new requests wait in the listen queue until one is free, and if the queue fills, nginx returns a 502 or 504.

That gives you the basic sizing problem. Too few workers and requests queue even though CPU is free. Too many workers and they compete for CPU, every request gets slower, and memory runs out. The right number depends on CPU cores, available RAM and how much memory one Magento request uses.

Choosing the process manager

PHP-FPM has three modes for pm:

  • static: a fixed number of workers, always running. Predictable memory use and no time spent forking under load. Good for dedicated web servers.
  • dynamic: a minimum and maximum, with spare workers kept ready. Good when the server also runs other services and you want PHP to give memory back when traffic is low.
  • ondemand: workers start only when requests arrive. Fine for low traffic sites and staging, but the fork cost shows up as latency on a store.

For a production Magento web node I usually prefer static or dynamic with a high minimum. Magento workers are heavy to start because the first request in a fresh process has to load a lot of code into memory, even with OPcache.

Calculating pm.max_children

Start by measuring the real memory use of a worker under normal traffic:

ps -ylC php-fpm8.3 --sort:rss | awk 'NR>1 {sum+=$8; n++} END {print n, "workers,", sum/n/1024, "MB average RSS"}'

RSS counts shared memory, including OPcache, in every process, so this number overstates the real cost a little. That is fine for sizing, because it errs on the safe side.

Then the rough formula is:

# RAM you give to PHP divided by average worker size
# Example: 12 GB for PHP, 120 MB per worker
# 12288 / 120 = about 100 workers

Next, compare that number with CPU. A Magento request is mostly CPU bound once database and cache are fast. If the server has 8 cores, 100 workers all busy at once means each gets a small slice of CPU and every response gets slower. A common practical range is two to four workers per core, then adjust from monitoring. Take the lower of the memory based and CPU based numbers.

A pool configuration based on that might look like this:

[magento]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm-magento.sock
listen.owner = www-data
listen.group = www-data
listen.backlog = 511

pm = dynamic
pm.max_children = 32
pm.start_servers = 16
pm.min_spare_servers = 16
pm.max_spare_servers = 24
pm.max_requests = 1000

pm.status_path = /fpm-status
request_slowlog_timeout = 5s
slowlog = /var/log/php8.3-fpm-magento-slow.log
request_terminate_timeout = 300s

pm.max_requests

Each worker restarts after this many requests. It protects against slow memory leaks in extensions. Do not set it very low. Every restart throws away the warmed up process. Something between 500 and a few thousand is typical.

Slow log

request_slowlog_timeout writes a PHP stack trace for any request that runs longer than the limit. It is one of the cheapest debugging tools you have. When a page randomly takes ten seconds, the slow log often shows exactly which function was waiting, for example a curl call to a third party API inside a block.

Status page

With pm.status_path set and exposed in nginx only to localhost or your monitoring host, you can see active and idle workers, the listen queue and max children reached. If max children reached keeps going up and the listen queue is not empty, the pool is too small or requests are too slow.

Separate pools for frontend and admin

Admin and API requests behave differently from storefront requests. An admin product save or a large export can use far more memory and time than a category page. Some setups run separate FPM pools and route by path in nginx, with a small pool and a higher memory limit for the admin. That way a heavy admin action cannot use up the workers the storefront needs. It adds configuration, so it is worth it on busy stores and not on small ones.

OPcache settings for Magento

OPcache keeps compiled PHP in shared memory. Magento has a very large codebase, and the default OPcache sizes are too small for it. When OPcache fills up, PHP keeps compiling files on every request and performance drops without any error.

opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 512
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 130000
opcache.validate_timestamps = 0
opcache.save_comments = 1
opcache.consistency_checks = 0

realpath_cache_size = 10M
realpath_cache_ttl = 7200

save_comments must stay on

Magento reads PHPDoc comments for some features, for example the Web API reads method annotations to build REST and SOAP services. With opcache.save_comments = 0 those break. Leave it at 1.

validate_timestamps and deploys

With opcache.validate_timestamps = 0, PHP never checks if files changed on disk. That saves a stat call per file per request. The cost is that after a deploy you must reload PHP-FPM, or the old code keeps running. Put systemctl reload php8.3-fpm into the deploy script. If you deploy to a new release folder and switch a symlink, the reload also makes sure PHP stops using cached paths of the old release.

Check that it is big enough

The easiest check is to call opcache_get_status(false) from a small script served by the same pool, not from the CLI, since CLI has its own OPcache. Look at free memory, the number of cached scripts compared with max_accelerated_files, and whether oom_restarts is above zero. If memory is almost full after a day of traffic, raise memory_consumption.

JIT

PHP 8 has a JIT compiler in OPcache. For a typical Magento request the gain is small because time is spent in I/O, database calls and array handling, not tight numeric loops. It can also make debugging harder. I leave it off unless a benchmark on the actual store shows a clear gain.

php.ini values Magento needs

A few settings from the Magento system requirements and the common failure points:

  • memory_limit: the sample nginx config passes 756M to FPM. For CLI commands like setup:di:compile and static content deploy, use at least 2G.
  • max_execution_time: keep it reasonable for web requests. Long admin operations should be cron jobs or queue consumers, not 15 minute HTTP requests.
  • max_input_vars: the default 1000 is too low for some admin forms with many options, such as large configurable products or role permissions. Raise it if admin saves lose data silently.
  • session.auto_start: must be off.
  • zlib.output_compression: leave it off and let nginx compress responses.

Make sure Xdebug is not loaded in production. Even when it is not actively debugging, the extension adds overhead to every request. The Xdebug guide covers how to keep it only in local environments.

CLI and cron

Cron jobs and queue consumers run through PHP CLI, which uses its own ini file. They do not count against the FPM pool, but they compete for the same CPU and RAM. A heavy cron group like indexing or a product import can starve the web pool on a small server. Count them when you size pm.max_children, and consider running them on a separate node when the store grows.

Signs you got it wrong

  • 502 or 504 errors during traffic peaks: workers exhausted, check max children reached and the slow log.
  • Server swapping or OOM killer messages: too many workers for the RAM, or a memory leak. Lower the pool and look at RSS per worker.
  • High CPU, low throughput: too many workers for the cores, or OPcache full and recompiling.
  • Old code after deploy: validate_timestamps off and no FPM reload.

PHP-FPM tuning sits next to the rest of the stack. See the nginx configuration guide for how requests reach the pool and the Redis setup guide for cache and session backends.

FAQ

How many PHP-FPM workers does Magento 2 need?

Measure the average worker memory, divide the RAM you give to PHP by it, and compare with two to four workers per CPU core. Use the lower number and adjust from the FPM status page.

Should opcache.validate_timestamps be off in production?

Yes, it saves file checks on every request. You must then reload PHP-FPM on every deploy, or old code stays in memory.

Can I set opcache.save_comments to 0 for Magento?

No. Magento reads PHPDoc annotations, for example to build Web API services, and those features break when comments are stripped.

Does PHP JIT make Magento faster?

Usually not by much, because Magento time goes to I/O, database and array work. Benchmark it on your own store before turning it on.

I am available for freelance Magento and Adobe Commerce work, including server tuning. If you need help with this, get in touch.

Related articles