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.phpbrute 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.