Skip to main content
Tracking Down High CPU on Linux

Tracking Down High CPU on Linux

KU
• 7 min read•64 views

A field-tested workflow for finding which process burns your CPU and whether the time is user, system, or iowait.

A box pages at 3am with CPU pinned. Before you reach for the reboot button, the goal is narrow and answerable: which process, and what kind of CPU time is it burning? Those two facts point at completely different fixes. This is the workflow I run every time.

For the wider picture (load average, memory pressure, disk saturation), see the Linux troubleshooting guide. Here we stay on CPU.

Step 1: find the process

Start where everyone starts, but read it properly. top sorted by CPU tells you the offender in seconds.

bash.bash
$ top -o %CPU
top - 03:14:22 up 41 days,  2:07,  1 user,  load average: 7.81, 6.02, 4.10
Tasks: 214 total,   3 running, 211 sleeping,   0 stopped,   0 zombie
%Cpu(s): 18.3 us, 71.2 sy,  0.0 ni,  6.1 id,  0.0 wa,  0.0 hi,  4.4 si,  0.0 st
MiB Mem :  15869.4 total,   4021.1 free,   8912.3 used,   2936.0 buff/cache

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  28471 app       20   0 4821140 612204  18932 R 388.4   3.8  91:22.14 api-server
   1042 root      20   0  912304  44120  12008 S  12.6   0.3  14:02.90 node_exporter

api-server at 388% (nearly four full cores) is your target. Note the summary line already tells a story: 71.2 sy means most of the machine's time is in the kernel, not application code. Hold that thought.

If top is too noisy, pidstat gives you a clean per-process rate over a fixed interval, which is easier to trust than an instantaneous snapshot.

bash.bash
$ pidstat 1 3
Linux 6.8.0-45-generic (web-03)   07/21/2026   _x86_64_   (4 CPU)

03:15:01      UID       PID    %usr %system  %guest    %CPU   CPU  Command
03:15:02     1001     28471   19.0    72.0    0.00   91.0     2   api-server
03:15:03     1001     28471   18.0    73.0    0.00   91.0     1   api-server
03:15:04     1001     28471   20.0    70.0    0.00   90.0     3   api-server

%usr vs %system split per process is the payoff here. htop (with F5 tree view and t for thread display) does the same job interactively if you prefer it.

Step 2: read the CPU breakdown

The %Cpu(s) line is the most under-used diagnostic on the box. Each field means something specific:

  • us (user): time in application code. High user time is normal for a busy app. The fix lives in the code or its config.
  • sy (system): time in the kernel on behalf of the process, syscalls, context switches, network stack. High sys usually means the app is hammering the kernel: excessive read/write, tiny buffers, lock contention, or a context-switch storm.
  • wa (iowait): the CPU is idle waiting for disk or network storage. High iowait is not a CPU problem, it is a storage problem wearing a CPU costume. Adding cores will not help.
  • si (softirq): soft interrupt handling, mostly network packet processing. High softirq points at network load or a single-queue NIC.
  • st (steal): on a VM, time the hypervisor gave to other tenants while your vCPU was runnable. High steal means a noisy neighbour. You cannot fix it from inside the guest; resize or move the instance.

So the box above (18 us / 71 sy / 4 si) is not a hot application loop. It is a syscall or context-switch problem. If instead you saw 40 wa, you would stop looking at CPU entirely and go chase the disk. If you saw 30 st, you would open a ticket with your cloud provider. Symptoms that also show up as load average are worth cross-checking against diagnosing high load average.

Step 3: find the hot thread

A process at 388% is really several threads. You want the one actually burning cycles, because a 40-thread app rarely melts down evenly.

bash.bash
$ top -H -p 28471
    PID USER      PR  NI    VIRT    RES  S  %CPU  %MEM     TIME+ COMMAND
  28493 app       20   0 4821140 612204  R  98.7   3.8  22:14.02 worker-3
  28494 app       20   0 4821140 612204  R  97.9   3.8  21:58.71 worker-4
  28471 app       20   0 4821140 612204  S   1.1   3.8   0:44.10 api-server

Two worker threads are doing all the damage. Grab their thread IDs (TIDs) for the profiler. ps -T -p 28471 gives the same mapping non-interactively:

bash.bash
$ ps -T -p 28471 -o pid,tid,pcpu,comm --sort=-pcpu | head
    PID     TID %CPU COMMAND
  28471   28493 98.7 worker-3
  28471   28494 97.9 worker-4
  28471   28471  1.1 api-server

Step 4: profile what those threads are doing

For high user time, sample the call stacks with perf top. It shows you the hottest functions live, no restart needed.

bash.bash
$ sudo perf top -p 28471
Samples: 214K of event 'cpu-clock', 4000 Hz
Overhead  Shared Object        Symbol
  41.20%  api-server           [.] json_tokenize
  18.60%  libc-2.39.so         [.] __memmove_avx_unaligned
  11.04%  api-server           [.] regex_match_backtrack
   6.71%  api-server           [.] gc_mark_sweep

That is a smoking gun: a JSON tokenizer and a backtracking regex are eating 50%+ of the thread. For a persistent record you can turn into a flame graph, record and report:

bash.bash
$ sudo perf record -F 999 -g -p 28471 -- sleep 20
[ perf record: Captured and wrote 4.8 MB perf.data (19842 samples) ]
$ sudo perf report --stdio | head -20
# Overhead  Command      Symbol
    41.2%   worker-3     [.] json_tokenize
    11.0%   worker-3     [.] regex_match_backtrack

Feed that perf.data through Brendan Gregg's stackcollapse-perf.pl and flamegraph.pl and you get the visual that makes the hotspot obvious to anyone.

For high system time, the profiler is strace. Count syscalls to see what the process asks the kernel to do:

bash.bash
$ sudo strace -f -c -p 28471
strace: Process 28471 attached
^C
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 68.44    4.812301           6    782140           read
 22.10    1.554002           4    381004           futex
  8.90    0.625810           7     88201           write

782k read calls and 381k futex calls in a few seconds is the answer to the 71% sys time: tiny unbuffered reads plus lock contention on a futex. The fix is buffering and reducing lock churn, not more CPU.

When you suspect a context-switch storm, vmstat confirms it. Watch the cs (context switches) and in (interrupts) columns:

bash.bash
$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 3  0      0 411204  92100 300288    0    0     8    40  980 412088 18 71  6  0  0
 4  0      0 410880  92100 300288    0    0     0     0  944 408221 19 72  5  0  0

400k context switches per second is pathological. That maps straight to the futex churn strace found: threads fighting over a lock, waking and sleeping constantly.

Common causes, quick reference

  • Busy loop / spin: 100% user on one thread, flat perf top on one function. Usually a missing sleep or a poll loop with no backoff.
  • GC pressure: perf top heavy in gc_* or your runtime's collector. Tune heap size or reduce allocation rate.
  • Regex / JSON hotspots: pathological backtracking or parsing giant payloads. Cap input size, precompile patterns, or swap the parser.
  • Context-switch storm: high sys, huge cs in vmstat, futex in strace. Lock contention or too many runnable threads.
  • iowait masquerading: high wa, low us/sy. Not CPU at all, go profile the disk.

The call we'd make

Read the %Cpu(s) line first. It decides everything. High user means profile the code with perf and fix the hot function. High system means count syscalls with strace and cut the kernel chatter, usually buffering or lock contention. High iowait means walk away from CPU and go chase storage. High steal means it is the hypervisor's fault, resize or migrate. Then drill from process to thread to function so your fix lands on the exact line that matters, not a guess. Nine times out of ten the summary line and one perf top tell you everything before the pager stops buzzing.

Explore topics:Linux
React
Share this post
KU

Kiril Urbonas

AI Engineer

553 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.