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.

We tested every step in this guide on Debian 13 with Nginx and PHP 8.4. Two sites, a shop and a blog, each had its own PHP-FPM pool. A slow search page on the shop got many requests at once, and we followed it with each tool below. Ubuntu works the same way. Change 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

Quick list

  • Which site: top -o %CPU, look at the USER column (one pool user per site).
  • Which page: pm.status_path, then curl .../fpm-status?full from the server.
  • Which visitors: log $request_time, then sort the access log.
  • Which code: request_slowlog_timeout and slowlog in the pool, read the entry from the bottom up.
  • Quick check: grep "executing too slow" /var/log/php8.4-fpm.log.

Official sources

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.

← Back to the security blog