Using the PHP-FPM Slow Log to Find What's Actually Slowing Down WordPress
· 9 min read
A client's WooCommerce store was intermittently slow. Not consistently — the homepage loaded in under a second most of the time, but two or three times an hour, random pages would take eight to twelve seconds. The nginx access log confirmed it: scattered 200 responses with upstream response times above 10s, mixed in with normal sub-second requests.
Page caching was working. Redis object cache was healthy, no evictions. The MariaDB slow query log was clean. Server load was low — 0.3 on a 4-core VPS. Yet something was holding PHP-FPM workers hostage for ten seconds at a time.
This is exactly the scenario the PHP-FPM slow log was built for.
What the slow log actually captures
Most WordPress developers know about the MariaDB slow query log. Fewer know that PHP-FPM has its own equivalent. When a PHP request exceeds a configured time threshold, PHP-FPM snapshots the call stack at that moment and writes it to a log file. You get a backtrace showing exactly which function PHP was executing when it crossed the line.
This is different from application-level profiling tools like Query Monitor or New Relic. Those require a page load to complete before they report anything. The PHP-FPM slow log captures what's happening during the slow request — even if it never finishes.
Enabling it
The slow log is configured per pool. On most setups, the pool configuration lives at /etc/php/8.3/fpm/pool.d/www.conf (adjust the PHP version to match yours). The relevant directives:
; Log requests slower than 3 seconds
request_slowlog_timeout = 3s
; Where to write the log
slowlog = /var/log/php-fpm/slow.log
; How deep to capture the backtrace (default 20)
request_slowlog_trace_depth = 30
On CloudPanel, the pool config is typically at /etc/php/8.3/fpm/pool.d/site.conf. On cPanel servers with EasyApache, check /opt/cpanel/ea-php83/root/etc/php-fpm.d/.
After editing, restart PHP-FPM:
sudo systemctl restart php8.3-fpm
I set request_slowlog_timeout to 3 seconds for this investigation. For ongoing monitoring, 5 seconds is less noisy while still catching genuine problems. Anything under 2 seconds generates too much log volume on a busy WooCommerce store.
Reading the output
Within an hour, the slow log had entries. Here's what a typical one looks like:
[03-Jun-2026 14:22:17] [pool www] pid 18429
script_filename = /var/www/html/index.php
[0x00007f3a2e0147d0] curl_exec() /var/www/html/wp-includes/Requests/Transport/cURL.php:221
[0x00007f3a2e014520] request() /var/www/html/wp-includes/Requests/Transport/cURL.php:163
[0x00007f3a2e014280] request() /var/www/html/wp-includes/class-requests.php:379
[0x00007f3a2e013f80] request() /var/www/html/wp-includes/class-wp-http.php:428
[0x00007f3a2e013c50] request() /var/www/html/wp-includes/class-wp-http.php:489
[0x00007f3a2e0138a0] get() /var/www/html/wp-includes/http.php:87
[0x00007f3a2e013560] wp_remote_get() /var/www/html/wp-content/plugins/flavor-of-the-month/includes/api.php:44
[0x00007f3a2e013210] fetch_remote_data() /var/www/html/wp-content/plugins/flavor-of-the-month/includes/widget.php:112
[0x00007f3a2e012e80] render() /var/www/html/wp-includes/class-wp-widget.php:394
Read it bottom to top. WordPress is rendering a widget, which calls fetch_remote_data() in a plugin, which calls wp_remote_get(), which ends up in curl_exec(). PHP-FPM caught the process sitting in curl_exec — waiting for an external HTTP request that hadn't come back.
The critical detail is in the plugin path: /wp-content/plugins/flavor-of-the-month/includes/api.php:44. That's the line making the blocking HTTP call.
The WordPress front-controller problem
There's a catch with WordPress slow logs. Because WordPress routes nearly all front-end requests through /index.php, the script_filename line is almost always the same:
script_filename = /var/www/html/index.php
This means you can't tell from the slow log alone which URL was being loaded. To correlate a slow log entry with an actual page request, you need to cross-reference timestamps against the nginx access log. This assumes your log_format includes $upstream_response_time as the last field — if it doesn't, add it:
grep "03/Jun/2026:14:22" /var/log/nginx/access.log | awk '$NF > 3 {print}'
If $upstream_response_time isn't the last field in your log format, replace $NF with the correct field number. This finds access log entries from the same minute with upstream response times above 3 seconds. On this site, the slow entries correlated with product category pages — the pages where the offending widget was displayed.
Aggregating the data
A single backtrace tells you what happened once. To find a pattern, you need to aggregate. After a few hours of logging, I ran a quick analysis:
grep -c "script_filename" /var/log/php-fpm/slow.log
That returned 47 entries in four hours. Then I extracted the top-of-stack function (the one PHP was actually executing when the timeout fired):
grep -A1 "script_filename" /var/log/php-fpm/slow.log | grep -oP '\] \K\S+\(\)' | sort | uniq -c | sort -rn | head
Output:
31 curl_exec()
9 mysqli_query()
4 flock()
3 sleep()
31 out of 47 slow requests were stuck in curl_exec(). That's a plugin making external HTTP calls that block the PHP worker while waiting for a response.
To find which plugins were responsible:
grep -A10 "curl_exec" /var/log/php-fpm/slow.log | grep "wp-content/plugins" | sort | uniq -c | sort -rn
28 [0x...] wp_remote_get() .../plugins/flavor-of-the-month/includes/api.php:44
3 [0x...] wp_remote_post() .../plugins/flavor-of-the-month/includes/license.php:19
One plugin, two call sites. Every time a category page loaded, this plugin's widget made a synchronous HTTP GET to an external API. When that API was slow or unreachable, the PHP-FPM worker sat idle for up to 5 seconds (the default wp_remote_get timeout) — or longer if the plugin sets a custom timeout — doing nothing but holding memory and a worker slot.
The 9 slow queries
The mysqli_query() entries were a separate issue. I extracted those backtraces:
grep -B20 "mysqli_query" /var/log/php-fpm/slow.log | grep "wp-content"
These all traced back to a WooCommerce analytics query running on wp-admin/admin-ajax.php. The admin dashboard was triggering a heavy reporting query when a shop manager had it open. Less urgent than the front-end issue, but still worth a EXPLAIN and an index review.
What the fix looked like
For the plugin making blocking HTTP calls on every page load, I had three options:
- Remove the plugin — the client wasn't actively using the feature it provided
- Cache the API response using a transient with a reasonable TTL
- Move the call to an async pattern — fire it via WP-Cron or Action Scheduler instead
Option 1 was the right call. The plugin had been installed two years ago for a promotion that ended eighteen months ago. Deactivating and removing it eliminated 28 of 31 curl_exec slow log entries immediately.
For the remaining three entries (the license check in the same plugin), removing the plugin solved those too.
Other patterns to watch for
After running slow logs across dozens of client sites, these are the most common backtraces I see:
curl_exec via wp_remote_get or wp_remote_post — by far the most frequent. Plugins checking licenses, fetching social media counts, pulling remote RSS feeds, or pinging analytics APIs. Every one of these blocks a PHP-FPM worker for the duration of the HTTP call.
mysqli_query via $wpdb->query — a slow database query. Cross-reference with the MariaDB slow query log for the actual SQL. Common culprits: wp_options queries on a bloated autoload set, wp_postmeta queries without proper indexes, and WooCommerce reporting queries.
flock — file locking contention on wp-content/debug.log or session files. Usually caused by WP_DEBUG_LOG being enabled on a production site with heavy traffic. Every PHP process tries to acquire a write lock on the same file.
sleep — not a bug, but a sign. Some plugins deliberately call sleep() for rate limiting or retry logic. If this shows up in your slow log, a plugin is wasting a PHP worker doing nothing.
Keeping it running in production
I leave the slow log enabled on all client servers with request_slowlog_timeout = 5s. The overhead is negligible — PHP-FPM only captures a backtrace when the threshold is exceeded, and on a healthy site that should be rare.
One thing to manage: the slow log file doesn't rotate by default. Add a logrotate configuration:
cat > /etc/logrotate.d/php-fpm-slow << 'EOF'
/var/log/php-fpm/slow.log {
weekly
rotate 4
compress
missingok
notifempty
postrotate
/usr/bin/systemctl reload php8.3-fpm > /dev/null 2>&1 || true
endscript
}
EOF
I also pair the slow log with request_terminate_timeout — a hard kill for PHP workers that run too long:
; Kill requests that take more than 120 seconds
request_terminate_timeout = 120s
Without this, a truly stuck PHP worker (waiting on an unresponsive external API with no timeout set) can hold a worker slot indefinitely. On a server with 15 max workers, a few stuck requests can exhaust the pool entirely.
The broader lesson
Application-level tools like Query Monitor are excellent for profiling requests you can reproduce. But for intermittent slowdowns that happen unpredictably — the ones clients describe as "the site is sometimes slow but I can't tell you when" — the PHP-FPM slow log is the tool that catches them in the act.
It requires SSH access and a willingness to read backtraces, which is why most site owners never touch it. But for anyone managing WordPress at scale, it's one of the most efficient diagnostic tools available. This is part of the server-level monitoring I run across all client sites — enable it, rotate the log, and check it when performance complaints come in. The backtrace usually points straight at the problem.
Stop Firefighting. Start Maintaining.
I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.
