Originally published on kuryzhev.cloud
A WordPress site that loads fine on a warm cache but crawls under real traffic is a recurring pattern in inherited infrastructure. Before reaching for a bigger instance or a caching plugin, WordPress performance profiling with php-fpm's status page and slow log gives you actual evidence instead of guesses. It is tempting to jump straight to "add more workers" or "enable object cache," which sometimes helps and sometimes just delays the same problem at a slightly higher traffic level.
Why this checklist
php-fpm sits between nginx (or Apache) and the PHP code that WordPress actually runs. It manages a pool of worker processes, and the questions that matter most — is the site CPU-bound, database-bound, or waiting on external APIs — tend to show up in its status page and slow log earlier than in higher-level dashboards. Skipping this data source means you're profiling blind.
The complication is that php-fpm's status page is not exposed by default in most distribution packages, and the slow log requires both a threshold and a log path before it records anything. Neither is dangerous to enable, but both are frequently forgotten until an incident forces someone to look for them under pressure. That's the wrong time to learn the syntax.
This checklist assumes a fairly standard setup: nginx or Apache as the front end, php-fpm on a currently supported PHP 8.x branch, and WordPress running as the application. Directive names have been stable across recent 8.x releases, but verify them against your installed version rather than assuming. It does not assume a specific hosting stack, since the same principles apply whether php-fpm runs in a container, a VM, or bare metal.
The goal is not to memorize commands. It's to build a repeatable habit: enable the status endpoint, enable the slow log, correlate both against real request patterns, and only then decide whether the fix is code, configuration, or capacity. For infrastructure-level context on how this fits into a broader monitoring strategy, see the DevOps_DayS knowledge base.
The checklist
-
Confirm the pool manager mode. Check whether
pmis set todynamic,static, orondemandin your pool config. Dynamic is the common packaged default; static gives more predictable memory usage under load and is easier to reason about while profiling. -
Enable the status page. Add
pm.status_pathto the pool configuration and expose it through the web server, restricted to internal addresses only. -
Enable the slow log with a realistic threshold. Set
request_slowlog_timeoutto something like 2s as a starting point, not 30s, or you'll miss most of what's worth seeing. It has no effect unlessslowlogis also set. - Set a slow log path that log rotation actually covers. A slow log with no rotation policy fills disks quietly over weeks.
-
Cross-check pool size against actual concurrency. Use the status page's
active processesandmax children reachedcounters, not just CPU graphs. - Watch for "max children reached" events specifically. This is a strong signal that php-fpm is undersized for current traffic rather than that PHP itself is slow.
-
Correlate slow log entries with the database, not just PHP. WordPress slow requests frequently trace back to unindexed queries or plugin-driven
wp_optionsautoload bloat. - Check request duration distribution, not just averages. A healthy-looking median can hide a long tail that only appears under concurrent load, which is why percentiles matter here.
- Verify opcache is enabled and not being invalidated constantly. Frequent invalidation from file changes (common on staging or auto-deploy setups) undercuts much of the benefit.
- Rule out external HTTP calls inside request handling. Slow log backtraces often point to blocking calls to third-party APIs, ad networks, or license-check servers inside plugin code.
- Confirm the status page format matches what your monitoring tool expects. php-fpm can emit plain text, JSON, XML, and HTML; pick JSON for anything feeding Prometheus or a similar system.
- Document the baseline before changing anything. Without a documented "before" state, you can't honestly claim the fix worked.
Here's a minimal pool configuration enabling both the status page and the slow log, with inline notes on the non-obvious parts.
[www]
; expose the status endpoint internally; the web server must proxy this path
pm.status_path = /status
; ping endpoint for liveness checks separate from application health
; ping.response defaults to "pong"; nginx must proxy this path too
ping.path = /ping
ping.response = pong
; slowlog path is required for request_slowlog_timeout to take effect
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 2s
; static avoids spawning workers mid-spike; max_children is the fixed pool size
pm = static
pm.max_children = 12
To pull the status page in a script-friendly format, request JSON output through the web server that proxies the endpoint. Quote the URL so the shell does not interpret ? and &:
curl -s "http://127.0.0.1/status?full&json" \
-H "Host: example.com" \
| jq '{active: .["active processes"],
maxed: .["max children reached"],
queue: .["listen queue"]}'
# "max children reached" > 0 over any recent window means the pool is undersized
# a nonzero, growing "listen queue" means requests wait before PHP even starts
Commonly missed items
The status page's full query parameter, which lists per-process detail including the currently executing script and its request duration, gets overlooked constantly. Without it, you see aggregate numbers but not which specific request is stuck right now — the difference between confirming a problem and diagnosing it.
It is also easy to enable the slow log once during an incident, find the offending plugin, then forget to revisit the threshold afterward. A permanent slow log with too aggressive a threshold generates noise that trains people to ignore it — the same failure mode as alert fatigue in any monitoring pipeline.
Watch out for: restarting php-fpm to apply pool changes without checking whether a graceful reload is available. A hard restart can drop in-flight requests. On Debian and Ubuntu the unit is version-qualified, so use something like systemctl reload php8.3-fpm; on RHEL-family systems it is typically systemctl reload php-fpm. Confirm the unit name with systemctl list-units 'php*fpm*' and check your distribution's documentation, since reload semantics differ between packages and versions.
Another frequently missed item is whether opcache.validate_timestamps is appropriate for the environment. Enabled, it makes PHP check file modification times, throttled by opcache.revalidate_freq; the cost depends on your filesystem, revalidation frequency, and PHP version, so treat it as something to measure rather than assume. Disabled in a frequently-deployed staging environment, it will serve stale bytecode silently until you reload the pool.
Watch out for: confusing php-fpm's slow log with PHP's own error log. They serve different purposes — the slow log writes a PHP backtrace of the script that exceeded the timeout, while the error log captures fatal errors and warnings — and conflating the two during an investigation wastes time chasing the wrong signal.
Finally, the status page's listen queue value is routinely ignored even though it's one of the clearest indicators of saturation: a nonzero and growing listen queue means requests are waiting for a free worker before PHP even starts executing.
Automation ideas
Scraping the JSON status endpoint on a schedule and feeding it into Prometheus via a lightweight exporter turns this checklist into an ongoing dashboard instead of a one-time investigation. Several community exporters for php-fpm exist; whichever you choose, verify it exposes max children reached and listen queue as first-class metrics, since those are the two values most worth alerting on.
A cron-based slow log summarizer, run hourly, can group entries by the triggering plugin or theme function and post a daily digest. That turns raw log noise into a short, reviewable list without requiring a full log aggregation platform for smaller sites.
Here's a small Prometheus alerting rule for the listen queue metric, assuming an exporter that surfaces it as phpfpm_listen_queue. Adjust the metric and label names to match your exporter:
groups:
- name: php-fpm
rules:
- alert: PhpFpmListenQueueGrowing
expr: phpfpm_listen_queue > 0
# avoid firing on single-request blips
for: 2m
labels:
severity: warning
annotations:
summary: "php-fpm listen queue nonzero for 2+ minutes"
For teams running php-fpm inside containers, wiring the status endpoint into a readiness probe is worth considering carefully — a probe that queries /status can surface pool exhaustion before it becomes a full outage, though it adds a dependency the probe logic must handle gracefully if the status page itself is misconfigured or unreachable. Consult the official php-fpm configuration documentation for the current list of directives, since defaults and available options have shifted across PHP 8.x releases.
None of this replaces application-level profiling tools, but it establishes the baseline layer almost everything else depends on. WordPress performance profiling that starts at the pool level tends to produce clearer answers than one that starts at the plugin level, because a caching layer built on top of an undersized php-fpm pool mostly moves the saturation point rather than removing it.
Further reading: official documentation
Top comments (0)