Optimizing PHP-FPM settings for performancefor Drupal 11 , 10 , 9 , and 8

Last updated :  

PHP-FPM is the traffic controller deciding how many requests your server processes simultaneously. Set it too conservatively and legitimate traffic queues up waiting for a free worker even though the server has spare capacity; set it too aggressively and a traffic spike exhausts your server's memory trying to run more PHP processes than it can actually hold, taking the whole site down instead of just slowing it.

What you'll learn in this lesson

  • What a PHP-FPM pool and its worker processes actually are
  • The math behind sizing pm.max_children correctly
  • The three process-management strategies and when each fits

The core pool settings

; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15

Each PHP-FPM "child" is a full PHP process capable of handling exactly one request at a time. pm.max_children is the hard ceiling — the 51st simultaneous request queues until a worker frees up, rather than the server trying (and potentially failing) to spawn unlimited processes.

The sizing math

# Rough formula: available RAM for PHP ÷ average memory per PHP process
# Example: 4GB available, ~80MB average process (check with `ps` on a
# running site under real load, not a guess) → max_children ≈ 50
free -m
ps -ylC php-fpm --sort:rss
The failure mode of setting this too high: if max_children is sized for more memory than the server actually has, a genuine traffic spike causes the OS to start killing processes (an OOM kill) or thrashing into swap — both far worse than the queuing that happens when the limit is sized correctly. "More workers" is not always faster; it's only faster up to the point your RAM actually supports.

Three process-management strategies

  • static — always exactly pm.max_children processes running, no scaling. Predictable memory use, wastes resources during quiet periods.
  • dynamic — scales between min_spare_servers and max_spare_servers based on load. The usual choice for a typical Drupal site with variable traffic.
  • ondemand — spawns workers only as requests arrive, killing idle ones after a timeout. Best for low-traffic or memory-constrained environments, at the cost of a small latency hit on a cold worker's first request.

Quick check: your server has 4GB RAM and each PHP-FPM worker uses roughly 100MB under real load. Is pm.max_children = 100 a safe setting? If you said no — that's 10GB of PHP processes alone on a 4GB server, well past what the OS can hold before something gets killed — you've done the sizing math correctly.

Key takeaways

  • pm.max_children is a hard concurrency ceiling — size it from actual measured per-process memory, not a guess.
  • Setting it too high risks OOM kills under real traffic spikes; too low causes unnecessary request queuing with spare capacity sitting idle.
  • dynamic is the right strategy for most Drupal sites; ondemand suits low-traffic/memory-constrained hosting; static suits predictable, consistently high load.
  • Measure real per-process memory with ps under actual traffic before sizing — don't copy a number from a blog post written for a different site.

Coming up next

You've tuned how PHP compiles and how many requests it can handle at once. Next, we look at the transport layer itself: enabling HTTP/2 to let browsers fetch more of your page's assets in parallel.