$linuxjunkies
>

How to Diagnose High CPU and Memory Usage

Learn to diagnose Linux CPU and memory problems using top, htop, load average, and the OOM killer log—from spotting runaway processes to preventing OOM kills.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • Terminal access with sudo privileges
  • htop installed (optional but recommended—apt/dnf/pacman install htop)
  • sysstat package for iostat if investigating I/O (apt/dnf/pacman install sysstat)
  • Basic familiarity with reading command-line output

A sluggish system, runaway fan, or stuttering desktop almost always traces back to a process eating CPU or memory. Linux gives you excellent visibility into exactly what is happening—if you know which tools to reach for and how to read what they say. This guide covers the essential diagnostic workflow: interpreting load average, inspecting live process stats with top and htop, and tracking down Out-of-Memory (OOM) kills after the fact.

Understanding Load Average First

Before opening any process monitor, understand what load average actually means. Run either of these:

uptime
# or
cat /proc/loadavg

You will see three numbers, for example 2.45 1.87 1.20. These are the 1-minute, 5-minute, and 15-minute exponential moving averages of the number of runnable (actively using CPU) plus uninterruptible-sleep (usually waiting on I/O) processes.

The rule of thumb: a load average equal to the number of logical CPU cores means the system is at 100% capacity. Find your core count:

nproc
# or for more detail
lscpu | grep -E '^CPU\(s\)|^Thread|^Core'

On a 4-core machine a sustained 1-minute load of 8 means the system is overloaded 2×. A spike that drops off quickly is usually fine; a 15-minute average that stays high is a real problem worth digging into.

Live Diagnosis with top

top ships on every Linux system and requires no installation. Launch it:

top

The summary header shows load average, total tasks, and a breakdown of CPU and memory. The columns that matter most in the process list:

  • %CPU — CPU share consumed by this process right now. Can exceed 100% on multi-core systems (e.g. 387% means nearly 4 full cores).
  • %MEM — Share of physical RAM in use.
  • RESResident Set Size: actual physical RAM this process holds, not swapped out. This is the most honest memory number.
  • VIRTVirtual address space claimed (includes mapped files, shared libraries). Almost always larger than RES and rarely alarming on its own.
  • S — Process state: R running, S sleeping, D uninterruptible I/O wait, Z zombie.

Useful top keyboard shortcuts

  • P — Sort by CPU (default)
  • M — Sort by memory (RES)
  • 1 — Toggle per-CPU breakdown in the header
  • k — Kill a process by PID
  • u — Filter by username
  • W — Save current configuration to ~/.config/procps/toprc

Many uninterruptible-sleep (D) processes with a high load average but low CPU often indicates a storage I/O bottleneck, not a CPU hog. Check with iostat -xz 2 (from the sysstat package) in that case.

Better Visibility with htop

Install htop if it is not already present:

# Debian / Ubuntu
sudo apt install htop

# Fedora / RHEL / Rocky
sudo dnf install htop

# Arch
sudo pacman -S htop
htop

htop adds colour-coded per-core CPU bars, a memory/swap meter, and mouse support. The process tree view (F5) is especially useful: it groups child processes under their parent, so you can see that ten php-fpm workers all belong to the same pool rather than counting them as unrelated offenders.

htop workflow for a high-CPU incident

  1. Press F6 and select PERCENT_CPU to sort by CPU descending.
  2. Note the PID of the top offender.
  3. Press F5 to switch to tree view and confirm whether it spawned child processes.
  4. Press F2 → Columns → add PROCESSOR to see which core the thread is pinned to (useful for detecting single-threaded bottlenecks).

Digging Deeper into a Specific Process

Once you have a PID, pull detailed stats without leaving the terminal:

# Open files and sockets
lsof -p <PID>

# Stack trace of what the process is doing (requires gdb or strace)
strace -p <PID> -c -e trace=all

# Threads and their CPU consumption
top -H -p <PID>

The /proc filesystem is always available without extra tools:

# Current status including VmRSS (physical RAM)
cat /proc/<PID>/status

# Command line that launched the process
cat /proc/<PID>/cmdline | tr '\0' ' '

# Environment variables at launch
cat /proc/<PID>/environ | tr '\0' '\n'

Tracking Down OOM Killer Events

When the kernel runs out of memory it invokes the OOM killer, which terminates a process to recover RAM. The killed process simply vanishes—no error, no core dump by default. The evidence lives in the journal:

# systemd-based systems (most modern distros)
journalctl -k --grep="oom_kill\|Out of memory\|Killed process" --no-pager | tail -40

A typical OOM line looks like:

kernel: Out of memory: Killed process 14823 (java) total-vm:4194304kB, anon-rss:3876920kB

This tells you the PID, the process name, virtual memory claimed, and actual anonymous RSS at kill time. The lines immediately above in the journal show the OOM score of candidate processes before the kernel chose its victim.

OOM score and oom_score_adj

The kernel assigns each process a score (0–1000); the highest score gets killed first. You can view and influence it:

# View score for a PID
cat /proc/<PID>/oom_score

# Lower the score so a critical daemon is less likely to be killed (-1000 = immune)
echo -500 | sudo tee /proc/<PID>/oom_score_adj

To make the adjustment persist across service restarts, add OOMScoreAdjust=-500 to the [Service] section of the relevant systemd unit and reload:

sudo systemctl edit --full <service-name>
sudo systemctl daemon-reload
sudo systemctl restart <service-name>

Checking Swap and Memory Pressure

High swap usage is a reliable indicator of memory pressure even without an OOM event:

free -h
vmstat 2 5
cat /proc/meminfo | grep -E 'MemFree|MemAvailable|SwapFree|Dirty|Writeback'

MemAvailable (not MemFree) is the kernel's estimate of how much memory can be given to new processes without swapping. If it is near zero while swap is active, you are under genuine memory pressure and should either reduce workload or add RAM.

Verification

After identifying and addressing the culprit (killing the process, restarting the service, adjusting limits), confirm the system has returned to baseline:

# Watch load average drop over ~5 minutes
watch -n 5 uptime

# Confirm memory is recovering
watch -n 3 free -h

Troubleshooting Common Situations

  • Load is high but CPU columns in top are all low: You are I/O-bound. Processes in state D contribute to load average. Run iostat -xz 1 and look for disks near 100% %util.
  • A process re-spawns immediately after you kill it: It is managed by systemd, a cron job, or a process supervisor. Find the unit with systemctl status <name> and stop the unit instead of the process.
  • Memory usage climbs slowly over days (memory leak): Log RES over time with ps -p <PID> -o rss= in a cron job and graph it, or use valgrind / heaptrack on the application binary.
  • OOM events but free -h shows available memory: Check /proc/meminfo for CommitLimit and Committed_AS. If overcommit is disabled (vm.overcommit_memory=2), the system can OOM-kill even with apparent free memory.
  • Zombie processes accumulating: Zombies hold no CPU or memory—they only consume a PID. The real problem is the parent not calling wait(). Find the parent with ps -o ppid= -p <zombie-PID> and investigate or restart it.
tested on:Ubuntu 24.04Fedora 40Debian 12Arch rolling

Frequently asked questions

What is a safe load average on my system?
A sustained load average below your logical CPU count (from 'nproc') is generally healthy. Brief spikes above that are normal; a 15-minute average consistently above core count warrants investigation.
Why does top show low CPU but my system still feels slow?
High load with low CPU usually means I/O bottleneck. Count how many processes are in state D (uninterruptible sleep) in top, then check disk saturation with 'iostat -xz 1' from the sysstat package.
How do I stop the OOM killer from targeting a specific service?
Add 'OOMScoreAdjust=-500' (or -1000 for full immunity) to the [Service] section of its systemd unit file, then run 'systemctl daemon-reload' and restart the service.
What is the difference between VIRT and RES in top/htop?
RES (Resident Set Size) is the actual physical RAM the process occupies right now and is the number to watch. VIRT includes mapped but not-yet-used memory, shared libraries, and mmap'd files—it is almost always much larger and rarely a problem on its own.
A zombie process shows up in top—how do I remove it?
Zombies hold no CPU or RAM; they just occupy a PID. Find the parent with 'ps -o ppid= -p <zombie-PID>' and either restart or fix the parent process so it calls wait() to reap its children.

Related guides