
Run uptime before you touch anything else. It costs nothing, takes a second, and tells you whether the server is actually under strain or just feels slow because of one hung request. Everything after this is just narrowing down which resource the load average is pointing at.
Those three numbers are the average number of processes wanting CPU time over the last 1, 5, and 15 minutes. A rising-then-falling pattern (1-min high, 15-min low) means you caught a spike that's already easing off. A pattern climbing the other way — 15-min lower than 1-min and both rising — means things are getting worse right now.
The number by itself is meaningless without knowing your core count. Check it with nproc. A load average of 4 on a 2-core box means work is queued and requests are waiting — that box is saturated. The same 4 on an 8-core box is well under half capacity and probably isn't your problem. Always divide load by nproc before deciding a server is "overloaded."
Treat this first check as a routing decision, not a diagnosis. High load with the ratio confirmed against core count tells you to keep digging into CPU, memory, and disk; a normal load average with a slow site tells you the problem lives one layer up, in the application itself, and you can skip straight to the later sections below.
If load average points at CPU, open top — it sorts by CPU usage by default. Press 1 inside top to switch to a per-core breakdown; a single fully-pegged core with the rest idle usually means a single-threaded process (a runaway PHP-FPM worker, a stuck cron job, an unoptimized regex) rather than genuine traffic load spread across the machine.
top gives you a snapshot; pidstat 1 5 from the sysstat package samples every second for five seconds so you can confirm a process is consistently hot rather than caught mid-spike. Once you have the PID, cross-reference it against your process manager (PHP-FPM pool, systemd unit, or Node process) to identify exactly which site or worker is responsible.
Run ps aux --sort=-%mem | head to list the top memory consumers by resident set size. That's the number that matters day-to-day — RSS is the actual physical RAM a process is holding, while VSZ (virtual size) also counts memory-mapped files and reserved-but-unused address space and is almost always far larger than what's really in use. Judge memory pressure from RSS and from free -h, not VSZ.
On WordPress and other PHP sites, the culprit is rarely one giant process — it's dozens of PHP-FPM child workers, each holding 60-300MB, that collectively exhaust RAM under traffic. If ps aux shows a long list of near-identical php-fpm lines rather than one outlier, the fix is tuning pm.max_children against available RAM, not chasing a single rogue process.
If CPU and memory both look fine but the server still feels sluggish, look at %iowait in the header row of top. A high iowait percentage means the CPU is sitting idle waiting on disk reads or writes to complete — it looks like "free" CPU in a casual glance, but it's actually the bottleneck.
A %util of 90%+ in iostat -x combined with rising await times means the disk itself is the bottleneck, not a single process. On a budget VPS with shared or spinning storage this often can't be fixed by tuning alone — you may genuinely need faster storage (NVMe) or fewer processes hitting disk concurrently, such as a backup job or log rotation colliding with peak traffic.
iotop needs root and shows per-process disk throughput live, sorted by I/O — run it with -oPa to only show active processes accumulated over time, which cuts through noise from idle daemons instantly.
When load average, CPU, memory, and iowait all look healthy but pages still load slowly, the bottleneck has moved up a layer — into the application itself. System metrics can't see a slow database query or a stalled external API call; they only see a process technically "running" the whole time it waits.
Add $request_time to your Nginx log format if it isn't already there, then grep for the slowest entries directly in the access log:
On the database side, enable MySQL's slow query log (slow_query_log = 1 and a sensible long_query_time like 1 second in /etc/mysql/mysql.conf.d/mysqld.cnf) and check /var/log/mysql/mysql-slow.log after restarting the service. An application-layer bottleneck often hides behind perfectly healthy CPU and RAM graphs — a single N+1 query pattern or an uncached external HTTP call can make a page feel slow even on an otherwise idle server.
Work the layers in order rather than jumping straight to the resource you suspect: uptime and nproc to see if load is actually high relative to core count; top and pidstat to catch a runaway CPU process; ps aux --sort=-%mem and free -h to rule out memory pressure or a PHP-FPM worker pile-up; iostat -x and iotop to check whether the CPU is really idle-waiting on disk; and finally Nginx's $request_time and MySQL's slow query log when everything upstream looks clean.
CloudStick's Server Stats panel already graphs CPU, memory, and disk history for every server you manage, so during an incident you can jump straight to step four — checking I/O and the application layer — instead of re-deriving trends by hand from raw top snapshots. The historical view also makes it obvious whether today's spike is a one-off or part of a pattern building over days.
Next time a server feels slow, resist the urge to restart services first — run uptime, note the load-to-core ratio, and work the checklist above in order. You'll usually have the responsible process identified within five minutes, and a fix in place before anyone outside your team notices.

