Linux perf Command: CPU Profiling and Performance Analysis

If your server feels slow but top and htop show nothing obvious, you need to go deeper. The perf command is the Linux performance analysis tool that most sysadmins never touch, and that’s a shame, because it can show you exactly what the CPU is doing at the instruction level.

I ignored perf for years. top, vmstat and iostat covered most of what I needed, and perf looked like a tool for kernel developers. That changed when [the server and the symptom]. Thirty seconds of perf record pointed straight at [what it found]. It has been in my toolbox ever since.

This guide walks through real-world usage of the perf command in Linux: profiling CPU usage, finding hot functions, tracing system calls, and interpreting the output without getting lost in theory.

What Is perf?

perf command in Linux showing perf report output with mysqld row_search_mvcc as the top CPU hotspot

perf is a performance analysis tool that lives in the Linux kernel source tree and talks directly to the kernel’s perf_events subsystem. It uses hardware performance counters, software events, and tracepoints to measure what the system is doing. Unlike tools that sample at the process level, perf can profile down to individual functions and even CPU instructions.

Because it is developed alongside the kernel, distros build perf to match the kernels they ship. That matters because some features depend on kernel support.

Note: perf is different from general monitoring tools like htop or atop. Those tell you that CPU is high. perf tells you why.

Installing perf

On most distros, perf is not installed by default. Install it with your package manager:

Debian / Ubuntu:

sudo apt install linux-perf

On Ubuntu, the package is split by kernel version instead:

sudo apt install linux-tools-$(uname -r) linux-tools-generic

Fedora / RHEL / CentOS:

sudo dnf install perf

Arch Linux:

sudo pacman -S perf

After installing, confirm it works:

perf --version

You should see something like perf version 6.x.x. If the version does not match your kernel, you may get warnings. On Ubuntu especially, make sure you install the package matching uname -r.

Basic Usage: What Can perf Do?

Run perf with no arguments to see the available subcommands:

perf

The most useful ones for day-to-day performance work:

  • perf stat: Count hardware and software events for a command or process
  • perf top: Live view of hottest functions system-wide, like top but for CPU cycles
  • perf record: Record profiling data to a file
  • perf report: Analyze recorded data interactively
  • perf list: List all available events
  • perf trace: Trace system calls (similar to strace but lower overhead)

perf stat: Count Events for a Command

perf stat output for ls /usr with instructions per cycle and branch misses highlighted

perf stat runs a command and prints performance counters when it finishes. It is the quickest way to get a feel for what a process is doing at the hardware level.

perf stat ls /usr

Output looks something like this:

 Performance counter stats for 'ls /usr':

              2.14 msec task-clock                #    0.728 CPUs utilized
                 1      context-switches          #  467.290 /sec
                 0      cpu-migrations            #    0.000 /sec
               281      page-faults               #  131.310 K/sec
         5,432,901      cycles                    #    2.538 GHz
         7,104,231      instructions              #    1.31  insn per cycle
         1,388,442      branches                  #  648.798 M/sec
            18,204      branch-misses             #    1.31% of all branches

       0.002938583 seconds time elapsed
       0.001778000 seconds user
       0.001133000 seconds sys

The key numbers to read here:

  • Instructions per cycle (IPC): Higher is better. Low IPC often means cache misses or stalls.
  • Branch misses: A high percentage can indicate the CPU is struggling to predict code paths.
  • Page faults: Unusually high counts may point to memory pressure.
  • Task-clock vs CPUs utilized: Less than 1.0 CPUs utilized means the process spent time waiting rather than computing.

You can also attach perf stat to a running process by PID:

sudo perf stat -p 1234 sleep 10

This samples PID 1234 for 10 seconds, then prints stats. Useful for profiling a running web server or database without restarting it.

Counting Specific Events

Use -e to specify events. See available events with perf list:

perf list | grep cache

Then count cache misses for a command:

perf stat -e cache-references,cache-misses,cpu-cycles myapp

A high ratio of cache misses to cache references means your application is memory-access bound, not compute bound. Faster CPUs will not help much there. Better data locality will.

perf top: Live CPU Hotspot View

sudo perf top live view listing kernel and user space functions by CPU overhead

perf top shows a live, updating view of the functions consuming the most CPU cycles across the whole system. Think of it as top for functions, not processes.

sudo perf top

You will see output like this:

Samples: 42K of event 'cpu-cycles', 4000 Hz, lost: 0/0 drop: 0/0
Overhead  Shared Object        Symbol
  18.32%  [kernel]             [k] __update_load_avg_cfs_rq
   9.14%  mysqld               [.] handler::ha_write_row
   7.88%  libc.so.6            [.] pthread_mutex_lock
   5.21%  [kernel]             [k] rep_movs_alternative
   4.03%  nginx                [.] ngx_http_process_request_line

The Overhead column is the percentage of CPU samples attributed to that function. The [k] marker means kernel space and [.] means user space. Kernel functions show [kernel] as their shared object. Everything else shows the library or binary it belongs to.

If you see a function you do not recognize, highlight it and press a to annotate the source or assembly. This requires debug symbols to be useful, but even without them, the call location gives you a starting point.

Limit to a specific process:

sudo perf top -p 1234

Or a specific CPU core:

sudo perf top --cpu 0,1

perf record and perf report: Deep Profiling

For anything serious, you want to record a profile and then analyze it. This is the standard workflow.

Step 1: Record

Profile system-wide for 30 seconds:

sudo perf record -g -a sleep 30
  • -g: Capture call graphs (stack traces). This is what makes the output readable.
  • -a: Profile all CPUs, not just the current process.
  • sleep 30: The recording runs for 30 seconds.

Profile a specific process:

sudo perf record -g -p 1234 sleep 30

Profile a specific command from start to finish:

sudo perf record -g ./myapp --options

This creates a perf.data file in the current directory. By default, perf record samples at around 4000 Hz. On a busy production box, add -F 99 to cut overhead and file size. The odd number is deliberate. It keeps samples from landing in lockstep with timers that fire at round rates like 100 Hz or 1000 Hz, which would skew the profile.

Step 2: Report

sudo perf report

This opens an interactive TUI. You can navigate with arrow keys, press Enter to expand a symbol, and a to annotate assembly. Press q to quit.

For a text dump instead:

sudo perf report --stdio | head -60

Output looks like:

# Overhead  Command  Shared Object       Symbol
# ........  .......  ..................  ...............................
    24.51%  mysqld   mysqld              [.] row_search_mvcc
    11.28%  mysqld   mysqld              [.] buf_page_get_gen
     8.73%  nginx    nginx               [.] ngx_epoll_process_events
     6.44%  php-fpm  libz.so.1.2.11      [.] deflate_slow

In this example, MySQL’s row_search_mvcc is eating nearly a quarter of CPU time. You would then look at slow query logs, missing indexes, or buffer pool sizing. Without perf, you would just see “mysqld is using CPU” and guess from there.

Reading Call Graphs

With -g enabled, you can expand symbols in the TUI to see the full call chain. This shows you not just which function is hot, but what called it. That context is often more useful than the function name alone.

For a flat call-graph text output:

sudo perf report --stdio --call-graph flat | head -80

If call chains look broken or end in [unknown], the binary was probably built without frame pointers. Ubuntu 24.04 and Fedora 38 and later build their packages with frame pointers, so -g works out of the box there. On other distros, switch to DWARF unwinding:

sudo perf record --call-graph dwarf -p 1234 sleep 30

DWARF mode writes much larger perf.data files, so keep these recordings short.

perf trace: Low-Overhead Syscall Tracing

perf trace summary for a mysqld process showing pread64 with 12.3ms max latency

perf trace is similar to strace but with significantly lower overhead. It is suitable for production systems where strace would slow things down too much.

Trace a running process:

sudo perf trace -p 1234

Trace only specific syscalls:

sudo perf trace -e openat,read,write -p 1234

Summarize syscall counts instead of streaming output:

sudo perf trace --summary -p 1234 sleep 10

Output:

             Summary of events:

 mysqld (1234), 10127 events, 96.3%

   syscall            calls  errors  total       min       avg       max
   --------------- -------- ------ -------- --------- --------- ---------
   pread64            4,201      0  892.3ms    0.1ms    0.2ms    12.3ms
   futex              1,832      0  423.1ms    0.0ms    0.2ms    45.2ms
   read                 891      0   14.2ms    0.0ms    0.0ms     1.1ms

That pread64 with a 12ms max latency on a database process is worth investigating. It could mean slow disk I/O, a cold buffer pool, or missing indexes causing full table scans.

perf list: Available Events

perf can measure a wide range of events beyond CPU cycles. List them all:

perf list

Filter by category:

perf list | grep -i cache
perf list | grep -i net
perf list | grep -i block

Some common useful events:

  • cache-misses: LLC (last-level cache) misses
  • page-faults: Memory page faults
  • context-switches: How often the kernel is switching between processes
  • cpu-migrations: Processes jumping between CPU cores
  • block:block_rq_issue: Block device I/O requests issued
  • net:net_dev_xmit: Network transmit events

Note: Hardware events like cache-misses require PMU (Performance Monitoring Unit) support. On VMs or some cloud instances, these may not be available. Check before you start. If dmesg | grep -i pmu mentions software events only, or perf stat -e cycles true prints <not supported>, the hypervisor is hiding the PMU. Software events like context-switches and page-faults work everywhere.

Flame Graphs: The Best Way to Read perf Data

flame graph from perf record of an Nginx, PHP-FPM and MySQL server

Interactive TUI reports are fine, but flame graphs are far easier to read at a glance, especially when sharing findings with a team.

The standard tool is Brendan Gregg’s FlameGraph scripts. Here is the full workflow:

git clone https://github.com/brendangregg/FlameGraph
sudo perf record -g -a sleep 30
sudo perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > flamegraph.svg

Open flamegraph.svg in a browser.

Newer perf builds (kernel 5.8 and later) can skip the Perl scripts entirely. Run perf script report flamegraph in the same directory as perf.data and it writes an interactive flamegraph.html. It needs the d3-flame-graph template, which some distros package and others download on first run. On a desktop, hotspot opens perf.data in a GUI with flame graphs built in.

In either view, the width of each box represents how much CPU time that function consumed. Tall stacks show deep call chains. Wide boxes at the top are your bottlenecks.

This helps most with slow web applications. If you see a wide box under your PHP or Python interpreter, you know the problem is in application code. If you see wide boxes in kernel I/O functions, the CPU is burning time in the storage or network stack, not in your code.

Practical Example: Diagnosing a Slow Web Server

This is the same workflow I use on my own Nginx and PHP-FPM servers. Say yours is slow under load, and top shows high CPU but nothing obvious. Here is what I do:

1. Record a profile under load:

sudo perf record -g -a sleep 60

While that runs, hit the server with traffic using ab or wrk.

2. Check the report:

sudo perf report --stdio | head -40

3. If you see Nginx functions dominating: Check worker connections, keepalive_timeout, and how much time goes to TLS. Heavy libcrypto or libssl symbols usually mean too many full handshakes, so confirm session resumption is enabled.

4. If you see PHP functions dominating: Look at which functions are hot. execute_ex being very high means time in user PHP code. _emalloc or zend_mm_alloc_* being high suggests excessive memory allocation, possibly from large arrays or object churn. Cross-reference with performance monitoring commands to confirm where pressure is coming from.

5. If you see kernel I/O functions dominating: The CPU is spending its time in the filesystem or block layer, often from a flood of small reads and writes. Batch the I/O, add caching, or fix the queries behind it if a database is involved.

One catch: perf record only samples code that is running on a CPU. Time spent blocked on disk does not show up at all. If the server is slow but CPU usage is low, check iowait and iostat first.

Permissions and Kernel Settings

perf requires elevated privileges for most useful operations. By default, unprivileged users can only profile their own processes and only with limited event sets.

Check the current paranoia level:

cat /proc/sys/kernel/perf_event_paranoid

Values and what they allow:

  • -1: Almost all events are open to all users.
  • 0: Unprivileged users lose raw and ftrace function tracepoints.
  • 1: Unprivileged users also lose system-wide CPU events.
  • 2: Upstream kernel default. Unprivileged users also lose kernel profiling, so they can only profile user space in their own processes.
  • 3 and above: Added by Debian and Ubuntu kernel patches. Blocks unprivileged users from perf entirely. Debian defaults to 3, and recent Ubuntu releases default to 4.

Root, or any user with CAP_PERFMON, is not affected by this setting. Running perf with sudo works at every level.

Containers share the host kernel, so the easiest way to profile a containerized app is from the host. Find the process ID with docker top or ps, then run sudo perf top -p PID or sudo perf record -g -p PID as usual. Running perf inside the container usually fails, because Docker’s default seccomp profile blocks perf_event_open unless the container has CAP_PERFMON or CAP_SYS_ADMIN.

To let non-root users profile temporarily:

sudo sysctl -w kernel.perf_event_paranoid=1

To make it permanent, drop it into a file under /etc/sysctl.d/:

echo 'kernel.perf_event_paranoid=1' | sudo tee /etc/sysctl.d/99-perf.conf

Note: On production systems, restore the original value after you are done profiling. Leaving paranoia at -1 or 0 permanently is a security risk.

You also need debug symbols to get meaningful function names. Without them, stripped binaries show raw hex addresses. Install them for the packages you care about:

Ubuntu/Debian: enable the debug symbol repository first (ddebs on Ubuntu, debian-debug on Debian). Package names end in -dbgsym and follow the binary package name:

sudo apt install php8.3-fpm-dbgsym

Fedora/RHEL:

sudo dnf debuginfo-install nginx php-fpm

Recent perf builds can also fetch symbols on demand from debuginfod servers, which Ubuntu, Debian and Fedora all run.

Kernel function names come from /proc/kallsyms, so perf run as root shows them without extra packages. You only need the kernel debug package to annotate kernel source lines:

sudo apt install linux-image-$(uname -r)-dbg      # Debian
sudo apt install linux-image-$(uname -r)-dbgsym   # Ubuntu (ddebs repo)
sudo dnf debuginfo-install kernel                 # Fedora/RHEL

Quick Reference

  • perf stat command: Count hardware events for a command
  • sudo perf stat -p PID sleep 10: Count events for a running process
  • sudo perf top: Live hotspot view
  • sudo perf top -p PID: Live hotspot view for one process
  • sudo perf record -g -a sleep 30: Record system-wide profile with call graphs
  • sudo perf report: Analyze recorded data
  • sudo perf trace -p PID: Trace syscalls for a process
  • sudo perf trace --summary -p PID sleep 10: Summarize syscall counts
  • perf list: List available events

Conclusion

perf fills a gap the usual monitoring tools leave open. When standard troubleshooting steps do not explain why your server is slow, the perf command is usually the next tool to reach for. It takes some getting used to. The core loop is short, though: perf record, then perf report, and a flame graph when you want the visual.

Always use -g. I learned that one the hard way: [my first profiles were flat lists of function names that told me almost nothing]. Without call graphs, you know which function is hot but not what called it. With them, you can trace the problem back to application code or a specific kernel path.

Start with perf stat on commands you run every day. You will learn a lot about how your software uses the CPU. Then move to perf top and perf record when something needs a deeper look.

Tags: , ,

Stop losing users to slow load times.

Blazing-fast, custom-configured NVMe Linux hosting.

Don't guess if your server is fast enough. Know it. Get a free performance audit of your current setup.

This blog is hosted by StackLinux, our parent company. 99.99% uptime.

Start Your Free Server Audit

You write the code. We'll manage the server.

Fully managed Linux hosting built for absolute speed.

Ditch the server maintenance headaches. Our experts custom-configure high-performance NVMe Linux servers specifically for your needs. Try us out. Your first month is entirely on us.

This blog is hosted by StackLinux, our parent company. 99.99% uptime.

Claim Your 30 Free Days

30 days of NVMe Linux hosting. $0 down.

Risk-free migration and a money-back guarantee.

We are so confident in our high-performance NVMe infrastructure that we'll audit your current server for free, migrate you without downtime, and give you 30 days to see the speed difference yourself.

This blog is hosted by StackLinux, our parent company. 99.99% uptime.

Deploy Free for 30 Days
Top ↑