Your site is slow, the server is sluggish over SSH, or monitoring reports CPU at 100%. High CPU has a handful of common causes on servers, and a systematic approach finds the culprit in minutes. The first step is always the same: which process, and what kind of CPU time?

1. Get the overview

uptime            # load averages vs number of cores (nproc)
top               # press P to sort by CPU, 1 to show each core

Read the %Cpu(s) line in top:

%Cpu(s): 85.3 us,  9.1 sy,  0.0 ni,  2.4 id,  1.2 wa,  0.0 hi,  0.5 si,  1.5 st
Field High means
us Applications (PHP, Node, MySQL queries…)
sy Kernel work: many syscalls, context switches, networking, or a lot of small I/O
wa Waiting for disk. Not really CPU. See I/O wait
st Steal: the hypervisor gave CPU to other guests
si Software interrupts: heavy network traffic

See top and load average.

2. Find the process

ps -eo pid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -15

Or htop with tree view (F5) to see parents and children, for example which PHP-FPM pool. See htop.

For a time-based view of which process used CPU:

pidstat -u 5 3          # from sysstat: per-process CPU every 5 s

3. Common culprits and fixes

Many PHP-FPM or Apache processes

Usually traffic, often bots, or slow uncached pages:

# who is hitting the site?
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
grep -c 'wp-login.php\|xmlrpc.php' /var/log/nginx/access.log

Fixes:

  • Add page caching (FastCGI cache, or a WordPress cache plugin). This is the biggest win. See Nginx FastCGI cache.
  • Block or rate-limit abusive IPs and paths (xmlrpc.php, wp-login.php brute force). See Nginx rate limiting and Fail2Ban for Nginx.
  • Enable OPcache, and an object cache (Redis).
  • Find slow scripts with the PHP-FPM slow log. See PHP-FPM tuning.

MySQL / MariaDB at 100%

Almost always slow queries, usually from missing indexes:

SHOW FULL PROCESSLIST;

Enable the slow query log, then add indexes for the worst queries. See MySQL slow query log and database indexing.

Node.js / Python at 100% on one core

A busy event loop or an infinite loop. Profile with node --prof / clinic.js, or py-spy top --pid PID. Run several processes to use all cores (PM2 cluster mode, more Gunicorn workers). See PM2.

Unknown processes: possible compromise

A process with a strange name (kdevtmpfsi, xmrig, random letters) using all CPU, often running from /tmp, /dev/shm or /var/tmp, is likely a crypto-miner dropped through a vulnerable web app, a weak password or an exposed service (Redis, Docker API).

ls -l /proc/PID/exe /proc/PID/cwd
sudo ss -tnp | grep PID              # connections to mining pools
crontab -l; sudo ls /etc/cron.d/ /var/spool/cron/crontabs/   # persistence

Killing it rarely helps for long, because it comes back via cron or the original hole. Treat the server as compromised. Back up the data, rebuild from a clean OS, patch the entry point and rotate credentials. See Linux server hardening.

High sy (system time)

Too many processes or threads competing, huge numbers of small network packets or connections, or an application making excessive syscalls. Check vmstat 1 (the cs context switches column) and use strace -c -p PID to see which syscalls dominate. See strace.

High st (steal)

Your VPS is waiting on the physical host. Short spikes are normal on shared virtualization. If steal stays high and performance suffers, contact support. CPU-heavy workloads that need guaranteed cores belong on a bigger plan or a dedicated server.

Scheduled jobs

CPU spikes at fixed times usually come from cron jobs: backups, wp-cron, image optimisation or log processing.

sar -u -s 02:00:00 -e 04:00:00      # history, if sysstat runs
systemctl list-timers; crontab -l

Run them at quiet hours with nice and ionice. See sar and process priority.

4. Short-term relief

sudo renice -n 15 -p PID                                # lower priority
sudo systemctl set-property --runtime app.service CPUQuota=50%   # cap a service
sudo systemctl restart php8.3-fpm                       # if workers are stuck

These buy time. The fix is caching, optimisation, blocking abuse or more CPU.

5. When it’s simply growth

If CPU is high because of legitimate, growing traffic, and the app is already cached and optimised, scale up. VPS.DO plans range up to 14 vCPU, and upgrades are handled by support ticket. Or split roles, with the database on its own server.

Quick reference

Step Command
Overview top (P = sort by CPU)
Top processes ps -eo pid,%cpu,cmd --sort=-%cpu | head
Per process over time pidstat -u 5
Who hits the site awk '{print $1}' access.log | sort | uniq -c | sort -rn
MySQL activity SHOW FULL PROCESSLIST;
History sar -u
Suspicious binary ls -l /proc/PID/exe

Related: high memory usage and vmstat.

  • Linux
  • Performance
  • Troubleshooting