PHP-FPM Tuning for WordPress: Stop Guessing, Start Measuring

Most WordPress sites run default PHP-FPM settings. Here's the actual math behind pm.max_children, pm.start_servers, and the monitoring to get them right.

Barry van Biljon
March 28, 2026
11 min read
PHP-FPM Tuning for WordPress: Stop Guessing, Start Measuring
Back to Blog

Key Takeaways

  • pm.max_children controls how many requests your server can handle simultaneously. Too high causes OOM crashes, too low causes 503 errors

  • The correct value is calculated from available RAM divided by average PHP process size, not guessed

  • Dynamic process management is correct for most WordPress sites. Static is only better under constant high traffic

  • Default PHP-FPM settings are designed for safety, not performance. They leave most of your server's capacity unused

  • Monitoring actual process usage over 24-48 hours tells you whether your settings are right

The setting that controls everything

There's one PHP-FPM setting that determines whether your WordPress site handles traffic smoothly or crashes under load: pm.max_children.

This setting controls the maximum number of PHP worker processes that can run simultaneously. Each incoming request to your WordPress site gets assigned to one worker. That worker processes the request (runs PHP, queries the database, generates HTML) and becomes available for the next request when it's done.

If all workers are busy and a new request arrives, the request waits in a queue. If the queue is full, the visitor gets a 503 error.

If pm.max_children is set too low, your server has idle RAM while visitors get 503 errors. If it's set too high, PHP workers consume all available memory and the server starts swapping to disk or the OOM killer terminates processes. Both of these crash your site.

Most WordPress sites run the default PHP-FPM settings: pm.max_children = 5 on many distributions. That's 5 simultaneous requests. On a 4GB server that could handle 30-40 workers, you're using 12% of your capacity.

Here's how to calculate the right number.


Step 1: measure your PHP process size

Every PHP-FPM worker uses memory. The amount depends on your WordPress installation: how many plugins are loaded, how complex your theme is, whether you're running WooCommerce, and what the request is doing.

Start by checking what your current workers use:

ps --no-headers -o rss,pid -p $(pgrep -d, php-fpm) | sort -rn

This shows each PHP-FPM process and its memory usage in KB. You'll see a range. A typical output:

  65432  12345
  62108  12346
  58944  12347
  55780  12348
  54200  12349   <- master process (smaller)

Calculate the average worker size (exclude the master process, it's always smaller):

ps --no-headers -o rss -p $(pgrep php-fpm) | \
  awk '{ sum += $1; n++ } END { print sum/n/1024 " MB" }'

Typical ranges:

WordPress setupAverage worker size
Simple blog, few plugins30-45 MB
Business site, 10-15 plugins45-65 MB
WooCommerce, 20+ plugins65-120 MB
WooCommerce + page builder + heavy plugins100-180 MB

Important: measure this under real traffic, not on an idle server. An idle worker uses less memory than one actively processing a WooCommerce checkout request. Visit several pages on your site (including WooCommerce pages if applicable) to warm up the workers, then measure.


Step 2: calculate pm.max_children

The formula:

max_children = (Total server RAM - reserved memory) / average worker size

Reserved memory includes:

  • Operating system: 200-300MB
  • MariaDB/MySQL: check with SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; plus ~100MB overhead
  • Redis (if used): check your maxmemory setting
  • Other services: monitoring agents, backup tools, anything else running on the server

Example calculation

Server: 4GB RAM (4096 MB)

ComponentMemory
OS and system300 MB
MariaDB (buffer pool + overhead)500 MB
Redis128 MB
Other (monitoring, SSH, etc.)100 MB
Available for PHP-FPM3068 MB

Average PHP worker size: 65 MB (measured on a WooCommerce site with 15 plugins)

max_children = 3068 / 65 = 47.2

Round down and leave a 10% buffer: set pm.max_children = 42

That's a big jump from the default of 5. On this server, the default left 37 potential workers sitting unused.

Don't over-provision

It's tempting to set max_children right at the calculated limit. Don't. Leave a 10-15% buffer for:

  • Temporary memory spikes during WooCommerce checkout
  • MariaDB needing extra memory for large queries
  • OPcache using more memory as you add plugins
  • WordPress cron running resource-heavy tasks

Running at 100% capacity means any spike in request size or a memory-intensive plugin causes the server to swap or crash.


pm.max_children doesn't work in isolation. The other pm.* settings control how PHP-FPM manages workers within that limit.

For pm = dynamic (recommended for most WordPress sites)

[www]
pm = dynamic
pm.max_children = 42
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500

pm.start_servers: how many workers to start when PHP-FPM launches. Set this to ~25% of max_children. These workers are ready immediately without the latency of spawning new ones.

pm.min_spare_servers: minimum idle workers to keep running. When the number of idle workers drops below this, PHP-FPM spawns new ones. Set to ~10-15% of max_children.

pm.max_spare_servers: maximum idle workers. When idle workers exceed this, PHP-FPM kills the extras to free memory. Set to ~35% of max_children.

pm.max_requests = 500: after a worker handles 500 requests, PHP-FPM kills it and starts a fresh one. This prevents memory leaks from poorly written plugins from accumulating indefinitely. Set between 200-1000. Lower values use slightly more CPU (more process restarts) but are safer against memory leaks.

For pm = static (high-traffic sites with consistent load)

[www]
pm = static
pm.max_children = 42
pm.max_requests = 500

Static mode keeps all 42 workers running at all times. No spawning latency, no scaling decisions. You use the full memory allocation permanently.

Use this only if:

  • Your traffic is consistently high (not bursty)
  • You can afford the constant memory usage
  • You've verified the server doesn't swap under full worker allocation

For pm = ondemand (low-traffic sites saving memory)

[www]
pm = ondemand
pm.max_children = 42
pm.process_idle_timeout = 10s
pm.max_requests = 500

Ondemand starts with zero workers and spawns them only when requests arrive. Workers shut down after process_idle_timeout seconds of inactivity.

This saves memory on sites with long idle periods but adds 50-100ms latency for the first request after idle (the time to spawn a worker). Not ideal for sites where speed matters for every request.


Step 4: PHP configuration for WordPress

Separate from PHP-FPM's process management, the PHP runtime settings also affect performance. Create a custom php.ini or add overrides:

; Memory limit per worker
memory_limit = 256M
 
; OPcache (compiled PHP bytecode cache)
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 60
opcache.validate_timestamps = 1
 
; Upload limits (adjust for WooCommerce product images)
upload_max_filesize = 64M
post_max_size = 64M
 
; Execution time
max_execution_time = 300
max_input_time = 300
 
; Realpath cache (reduces filesystem stat calls)
realpath_cache_size = 4096K
realpath_cache_ttl = 600

OPcache explained

OPcache stores compiled PHP bytecodes in shared memory. Without OPcache, PHP parses and compiles every .php file on every request. With OPcache, the compiled version is reused.

The impact on WordPress is significant. WordPress loads 50-100+ PHP files per request (core, theme, plugins). OPcache eliminates the parsing overhead for all of them.

opcache.max_accelerated_files = 10000: the number of PHP files OPcache can cache. WordPress with 20 plugins easily has 3,000-5,000 PHP files. Set this higher than your total file count.

opcache.revalidate_freq = 60: how often (in seconds) OPcache checks if a file has changed. On production, set to 60 or higher. OPcache skips the filesystem check for 60 seconds, reducing stat calls. When you deploy code changes, clear OPcache manually or restart PHP-FPM.


Step 5: monitor and adjust

The calculations from Step 2 are a starting point. Real-world traffic patterns, plugin behavior, and seasonal load variations mean you should monitor and adjust over time.

Check for max_children warnings

grep "max_children" /var/log/php-fpm.log

Or on systems using journald:

journalctl -u php-fpm --since "24 hours ago" | grep "max_children"

The line you're looking for:

WARNING: [pool www] server reached pm.max_children setting (42), consider raising it

If you see this during normal traffic, increase pm.max_children (and make sure you have the RAM). If you only see it during traffic spikes, your setting is probably appropriate and the queue handled the burst.

Check PHP-FPM status page

Enable the status page in your www.conf:

pm.status_path = /fpm-status

Restrict access in your web server config (Caddy, Nginx) to localhost or your IP only, then query it:

curl -s http://localhost/fpm-status

Key metrics in the output:

  • active processes: workers currently handling requests
  • idle processes: workers waiting for requests
  • listen queue: requests waiting for a worker (should be 0 most of the time)
  • max listen queue: highest queue length since last restart (should be low)
  • max children reached: how many times max_children was hit (should be 0 or very low)

Monitor memory usage over time

Track your server's memory usage over 24-48 hours:

# Quick snapshot
free -m
 
# Watch over time (every 60 seconds)
while true; do
  echo "$(date): $(free -m | awk 'NR==2{print $3"/"$2" MB"}')"
  sleep 60
done >> /var/log/memory-usage.log

If available memory stays above 200MB under peak traffic, your settings are safe. If it dips below 100MB regularly, reduce pm.max_children or add RAM.


Before and after: what tuning actually does

On a client site running on a 4GB VPS with WooCommerce and 18 active plugins:

Before (default PHP-FPM settings)

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Under a load test simulating 30 concurrent users:

  • Average TTFB: 1.8 seconds
  • Requests hitting queue: 83%
  • 503 errors: 12% of requests
  • Server memory usage: 1.1 GB of 4 GB (72% idle RAM)

After (tuned PHP-FPM settings)

pm = dynamic
pm.max_children = 35
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 500

Same load test, 30 concurrent users:

  • Average TTFB: 0.3 seconds
  • Requests hitting queue: 0%
  • 503 errors: 0%
  • Server memory usage: 3.1 GB of 4 GB (healthy headroom)

TTFB dropped from 1.8 seconds to 0.3 seconds. Same server. Same WordPress site. Same traffic. The only change was telling PHP-FPM to use the memory that was already there.


Common mistakes

Setting max_children based on CPU cores

Some guides suggest setting max_children to 2x or 4x your CPU cores. This ignores memory entirely. A 1-core VPS with 4GB RAM can handle more PHP workers than a 4-core VPS with 1GB RAM. Memory is the constraint, not CPU.

Using pm = static on a server that also runs the database

Static mode allocates all workers at startup. If MariaDB needs extra memory for a large query or buffer pool growth, there's nothing left. Use dynamic on shared-resource servers so idle workers can be killed to free memory when needed.

Never restarting PHP-FPM workers

Without pm.max_requests, workers run indefinitely. PHP plugins with memory leaks (and there are many) cause workers to grow from 60MB to 200MB+ over time. Eventually the server runs out of memory. Set pm.max_requests between 200-500 to automatically recycle workers.

Tuning PHP-FPM before fixing the database

If your database queries take 2 seconds each, a PHP worker is occupied for 2 seconds per request. You need 30 workers to handle 15 requests per second. After cleaning the database and adding indexes, those queries take 0.1 seconds. Now you need 2 workers for the same 15 requests per second. Fix the data first, then tune the server.


Barry van Biljon

Written by

Barry van Biljon

Connect on LinkedIn

Full-stack developer specializing in high-performance web applications with React, Next.js, and WordPress.

Ready to Get Started?

Have questions about implementing these strategies? Our team is here to help you build high-performance web applications that drive results.

Frequently Asked Questions

No. Shared hosting manages PHP-FPM at the server level across all accounts. You don't have access to the configuration. PHP-FPM tuning requires a VPS, dedicated server, or containerized setup where you control the PHP-FPM configuration files. If you're on shared hosting and hitting performance limits, this is one of the reasons to move to a VPS.