Server slow? Find which site and which page is causing it
When a server with several PHP sites gets slow, the hard part is knowing where to look. This guide goes from the whole server down to the exact line of code, in four steps: which site, which page, which visitors, and which function.
8.4 to your PHP version.1. Which site? Look at top
top -o %CPU
Look at the top lines. If each site has its own PHP-FPM pool with its own user, the USER column tells you which site is busy. In our test, four php-fpm processes owned by shop were at the top, each using about half a CPU. The blog didn't show up at all. If every site runs as www-data, top can't tell them apart. That's one more reason to give each site its own pool.
Use top, not ps. In our test, ps showed the same busy workers at only 3% CPU, because it shows the average over the process's whole life, not what it's doing now.
Also check if it's PHP at all. If mariadbd or mysqld is at the top, see finding slow database queries. If memory is the problem (free -m shows almost nothing available, and swap is in use), see out-of-memory errors and swap.
2. Which page? Ask PHP-FPM
PHP-FPM can show what each of its workers is doing right now. Add this to the pool file of the busy site, for example /etc/php/8.4/fpm/pool.d/shop.conf:
pm.status_path = /fpm-status
Then, in the site's Nginx file, allow only the server itself to see it:
location = /fpm-status {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_NAME /fpm-status;
fastcgi_param SCRIPT_FILENAME /fpm-status;
fastcgi_pass unix:/run/php/shop.sock;
}
Reload both (sudo php-fpm8.4 -t && sudo systemctl reload php8.4-fpm, then sudo nginx -t && sudo systemctl reload nginx), and ask it from the server:
curl -s -H "Host: shop.example" http://127.0.0.1/fpm-status
In our test, it said active processes: 4 and idle processes: 0: every worker was busy. Add ?full to see what each one is doing:
curl -s -H "Host: shop.example" "http://127.0.0.1/fpm-status?full" | grep -E "^(state|request duration|request URI)"
That showed exactly which page was the problem:
state: Running
request duration: 5968226
request URI: /search.php?q=4
The duration is in microseconds, so that request had been running for 6 seconds. Keep the allow 127.0.0.1 line: this page shows the addresses visitors open, and nobody outside should see it.
3. Which visitors? Read the access log
Nginx doesn't log how long each request took, unless you ask. Add a log format in /etc/nginx/conf.d/timing.conf:
log_format timing '$remote_addr [$time_local] "$request" $status $request_time "$http_user_agent"';
Then use it in the site's server block, and reload Nginx:
access_log /var/log/nginx/shop.access.log timing;
Now you can ask the log three questions. Which requests were slowest? (the time is in seconds)
awk -F'"' '{split($3, a, " "); print a[2], $2}' /var/log/nginx/shop.access.log | sort -rn | head
In our test, the search page took 12 to 15 seconds while it was busy. Which pages get the most requests?
awk -F'"' '{split($2, r, " "); sub(/\?.*/, "", r[2]); print r[2]}' /var/log/nginx/shop.access.log | sort | uniq -c | sort -rn | head
Who sends them?
awk '{print $1}' /var/log/nginx/shop.access.log | sort | uniq -c | sort -rn | head
If one address or one bot sends most of the requests to a slow page, slow it down with Nginx rate limiting. Search pages, filters and "add to cart" links are favourite targets for bots, because each one costs your server real work.
4. Which code? Turn on the slow log
PHP-FPM can write down exactly where in the code a slow request is. Add to the pool file:
request_slowlog_timeout = 5s
slowlog = /var/log/php8.4-fpm-shop-slow.log
Reload PHP-FPM. Every request that takes longer than 5 seconds now gets an entry like this one from our test:
[pool shop] pid 9612
script_filename = /var/www/shop/search.php
[0x00007fdcfe614240] md5() /var/www/shop/search.php:3
[0x00007fdcfe614120] slow_filter() /var/www/shop/search.php:2
[0x00007fdcfe6140b0] find_products() /var/www/shop/search.php:4
Read it from the bottom up: find_products() called slow_filter(), which was busy in md5(). In WordPress, the file paths show which plugin or theme it is. Send the entry to the plugin's developer, or to whoever wrote your code.
The main PHP-FPM log also names each slow request, even without the slow log file:
sudo grep "executing too slow" /var/log/php8.4-fpm.log | tail
In our test, that showed lines like script '/var/www/shop/search.php' (request: "GET /search.php?q=3") executing too slow (2.301099 sec). The status page's slow requests counter went up with each one.
The slow log reads the running PHP process from outside, using a Linux feature called ptrace. If the slow log stays empty and the main log says failed to ptrace(ATTACH) child, your server doesn't allow it. We saw this in a Docker container, which blocks ptrace by default. Most servers and VPSs allow it.
Then fix it
- One slow page, many visitors: cache it, or make it faster. Our OPcache and PHP-FPM guide covers PHP itself.
- Slow database queries behind it: find them with the slow query log.
- Bots: rate limiting.
- Heavy work in a web request (imports, emails, reports): move it to a background job. See queue workers.
- All workers busy, all the time: the pool may need more workers, if you have the memory. See fixing 502 and 504 errors, section 6.
Quick list
- Which site:
top -o %CPU, look at theUSERcolumn (one pool user per site). - Which page:
pm.status_path, thencurl .../fpm-status?fullfrom the server. - Which visitors: log
$request_time, then sort the access log. - Which code:
request_slowlog_timeoutandslowlogin the pool, read the entry from the bottom up. - Quick check:
grep "executing too slow" /var/log/php8.4-fpm.log.
Official sources
- PHP-FPM settings (php.net) (
pm.status_path,request_slowlog_timeout,slowlog) - PHP-FPM status page (php.net)
- Nginx: log_format and $request_time
Next steps: give each site its own PHP-FPM pool so you can always tell them apart, and set up uptime monitoring to hear about slowdowns early.
ServerNest is being built to show each site's PHP workers, slow pages and resource use on one screen. Join early access to try it first.