DevOps & SRE No. 161

Linux Troubleshooting skill for Claude Code

Name the saturated resource, with evidence. Claude runs a read-only 60-second checklist before touching anything, then follows the evidence to the saturated resource: run queue, D-state tasks, swapping, disk await, socket backlogs or fd limits. A bundled vmstat analyzer ranks suspects from captured output, and every finding ends with the responsible process, the cause and a mitigation that waits for your approval.

  • namelinux-troubleshooting
  • size174 lines · ≈2,050 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Diagnoses slow or failing Linux hosts with a USE-method triage - load and CPU saturation, memory pressure, OOM kills and swap, disk latency and full filesystems or inodes, network sockets, retransmits and DNS, file descriptor limits, and stuck processes - using uptime, vmstat, mpstat, pidstat, iostat, free, df, ss, dmesg, journalctl, strace, lsof and /proc, plus a bundled vmstat analyzer. Use when a server is slow, load is high, a process was killed, a disk is full, connections time out, or the user pastes top, vmstat or iostat output.

Try asking

  • Load average on this box is 40 but CPU looks idle. What is going on?
  • Here is our vmstat and iostat output from the slowdown. Find the bottleneck
  • The disk says full but du does not add up. Track down where the space went

Use it when

  • A Linux server is slow, overloaded or unresponsive
  • A process was OOM-killed or hit too many open files
  • A filesystem is full or out of inodes
  • Connections time out or get refused from a host
  • You have top, vmstat, iostat or ss output to interpret

Skip it when

How to check the result

The report names one saturated resource backed by at least two tools' output, identifies the responsible process, and the metric returns to baseline after the approved mitigation.

The common mistake it prevents

Deleting a huge log file that a running process still holds open, which frees no space until the writer is restarted.

~/.claude/skills/linux-troubleshooting/SKILL.md

--- name: linux-troubleshooting description: Diagnoses slow or failing Linux hosts with a USE-method triage - load and CPU saturation, memory pressure, OOM kills and swap, disk latency… license: MIT ---

Triage is done when you can name the saturated resource (CPU, memory, disk, network, or a limit), show the evidence from at least two tools, identify the process or cause responsible, and state a mitigation with its risk. “Load is high” is a symptom, not a diagnosis.

Ground rules

  • Observe before acting. Capture the 60-second checklist output first; the evidence disappears once you restart things.
  • Read-only commands first. Killing processes, dropping caches, restarting services, deleting files or rebooting require explicit user confirmation, especially in production.
  • Never rm a large log that a process holds open; the space is not freed. Truncate (: > file) or restart the writer, after confirming.
  • strace/perf add overhead; attach briefly (-p PID for seconds) on production.
  • Prefer facts from the kernel (/proc, dmesg, PSI) over intuition from top.

The 60-second checklist

Run in order and save the output (sysstat package provides mpstat/pidstat/iostat/sar):

uptime                                   # load averages: 1/5/15 min trend
dmesg -T | tail -50                      # OOM kills, disk errors, NIC resets, segfaults
journalctl -p warning --since "1 hour ago" --no-pager | tail -50
vmstat 1 10                              # r, b, si/so, wa, st
mpstat -P ALL 1 3                        # per-CPU imbalance, %iowait, %steal
pidstat 1 3                              # which process uses CPU
iostat -xz 1 3                           # per-device util, await, aqu-sz
free -m                                  # available memory, swap use
df -hT; df -i                            # space and inodes
ss -s; ss -tanp state established | head # socket summary, top connections
cat /proc/pressure/{cpu,memory,io}       # PSI stall percentages (kernel 4.20+)

Analyze captured vmstat output with the bundled script:

vmstat 1 30 > /tmp/vmstat.txt
python3 scripts/vmstat_triage.py /tmp/vmstat.txt --cpus "$(nproc)"

It flags run-queue saturation (r > CPUs), blocked tasks (b), swapping (si/so), high iowait, steal, and context-switch storms, prints the dominant suspect, and exits 1 when any resource is saturated.

Decision table

EvidencePoints toNext command
r > CPU count, us+sy near 100CPU saturationpidstat -u 1, top -H -p PID, perf top -p PID
High load but low CPU, b > 0, high waDisk I/O wait (or NFS)iostat -xz 1, pidstat -d 1, iotop -o
si/so non-zero continuously, low availableMemory pressure / swapping`ps aux —sort=-rss
st > 5-10%Noisy neighbor / CPU credit exhaustion (cloud)Check instance type, burst credits, move host
sy high, huge cs/inLock contention, syscall storm, too many threadspidstat -w 1, perf top, strace -c -p PID
One CPU at 100%, others idleSingle-threaded bottleneck or IRQ affinitympstat -P ALL 1, cat /proc/interrupts
“Killed” / exit 137OOM killer`dmesg -T
Writes fail “No space left on device”Full disk or full inodesdf -h, df -i, `du -xh —max-depth=1 /
Connections time out / refusedBacklog, conntrack, port exhaustion, firewall, DNSss -ltn (Recv-Q vs Send-Q), ss -s, nstat, dig
“Too many open files”fd limit`ls /proc/PID/fd
Process stuck, not using CPUBlocked in syscall (D state) or waiting on lock/networkcat /proc/PID/stack, cat /proc/PID/wchan, strace -p PID

Deep dives

CPU

top -o %CPU                       # press 1 for per-CPU, H for threads
ps -eo pid,ppid,stat,pcpu,pmem,etime,cmd --sort=-pcpu | head -15
pidstat -t -p <PID> 1 5           # per-thread CPU
perf top -p <PID>                 # hot functions (needs perf, symbols)
perf record -F 99 -g -p <PID> -- sleep 15 && perf report   # then flame graph if needed

Load average counts runnable plus uninterruptible (D) tasks on Linux. High load with idle CPU means D-state tasks: usually disk or NFS.

Memory

free -m                           # use "available", not "free"
grep -E "MemAvailable|Dirty|Writeback|Slab|SReclaimable|Committed_AS" /proc/meminfo
ps aux --sort=-rss | head -10
cat /sys/fs/cgroup/<path>/memory.events      # cgroup v2: oom, oom_kill, high counts
slabtop -o | head -15             # kernel slab growth (dentry/inode caches)
  • Page cache is not a leak; available accounts for reclaimable cache.
  • Containers get OOM-killed at their cgroup limit while the host shows free memory.
  • RSS steadily rising over days in one process: application leak (see memory-leak-hunting).

Disk

iostat -xz 1 5                    # r_await/w_await ms, aqu-sz, %util
pidstat -d 1 5                    # per-process read/write kB/s
du -xh --max-depth=1 /var | sort -h | tail
lsof +L1                          # deleted files still held open (space not freed)
find / -xdev -type f -size +1G -printf '%s %p\n' 2>/dev/null | sort -n | tail
MetricHealthy (SSD/NVMe)Problem
await< 1-5 ms> 20 ms sustained
aqu-sz< 1 per deviceGrowing queue
%utilNot reliable for NVMe/RAID (parallel devices)Use await and queue instead

Inodes full (df -i 100%) with space left: millions of small files (sessions, cache, mail queue). Find with find /path -xdev -type d -exec sh -c 'echo "$(ls -A "$1" | wc -l) $1"' _ {} \; | sort -n | tail.

Network

ss -ltnp                          # listening sockets; Recv-Q > 0 on LISTEN = accept backlog full
ss -tan state time-wait | wc -l   # TIME_WAIT pile-up
ss -tin dst 10.0.2.15             # per-connection rtt, retrans, cwnd
nstat -az | grep -E "TcpRetransSegs|ListenOverflows|ListenDrops|TcpExtTCPTimeouts"
ip -s link                        # interface errors/drops
conntrack -S 2>/dev/null; sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
dig +short api.example.com; resolvectl status    # DNS path
curl -w '@-' -o /dev/null -s https://api.example.com <<< 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer}\n'
  • ListenOverflows increasing: app accepts too slowly or backlog too small (net.core.somaxconn, app listen backlog).
  • Ephemeral port exhaustion on clients making many outbound connections: reuse connections (keepalive pools) before tuning ip_local_port_range.
  • nf_conntrack: table full in dmesg drops packets silently.

Processes and limits

cat /proc/<PID>/status | grep -E "State|Threads|VmRSS"
cat /proc/<PID>/limits
ls /proc/<PID>/fd | wc -l
lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn | head   # fd types
strace -f -tt -T -p <PID> -o /tmp/trace.txt   # Ctrl-C after a few seconds
strace -c -p <PID>                              # syscall summary
systemctl show <unit> -p LimitNOFILE            # systemd overrides ulimit

Gotchas

  • top %CPU is per core (400% = 4 cores busy). vmstat percentages are of all CPUs.
  • The first line of vmstat and iostat is the average since boot; ignore it.
  • ulimit -n in your shell is not the service’s limit; check /proc/PID/limits.
  • Containers hide host pressure: run the checklist on the node, and read cgroup files for the container (memory.current, memory.max, cpu.stat throttled_usec).
  • CPU throttling under a cgroup CPU limit looks like latency, not high CPU. Check nr_throttled in cpu.stat.
  • Time skew breaks TLS, auth tokens and distributed locks: timedatectl and chronyc tracking.
  • echo 3 > /proc/sys/vm/drop_caches fixes nothing and causes an I/O storm.

Report

**Host:** api-3 (8 vCPU, 16 GiB)  **Window:** 14:05-14:20 UTC
**Saturated resource:** disk I/O on nvme1n1 (w_await 180 ms, aqu-sz 34)
**Evidence:** vmstat b=6-11, wa=48%; pidstat -d shows postgres checkpointer 220 MB/s;
  dmesg clean; CPU us 12%
**Cause:** max_wal_size too small -> checkpoints every 40 s during bulk import
**Mitigation (proposed, needs approval):** pause import job; raise max_wal_size to 8GB
**Follow-up:** alert on disk await p95 > 50 ms for 10 min

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

scripts/vmstat_triage.py 98 lines · 3.7 KB
#!/usr/bin/env python3
"""Summarize `vmstat 1 N` output and flag saturated resources.

Usage:
  vmstat 1 30 > vmstat.txt
  python3 scripts/vmstat_triage.py vmstat.txt --cpus 8
  vmstat 1 30 | python3 scripts/vmstat_triage.py - --cpus "$(nproc)"

Skips the first sample (averages since boot). Exit 1 if any resource is saturated.
"""
import argparse
import statistics
import sys


def parse(lines):
    header = None
    rows = []
    for line in lines:
        parts = line.split()
        if not parts:
            continue
        if "r" in parts and "b" in parts and "us" in parts:
            header = parts
            continue
        if header and all(p.lstrip("-").isdigit() for p in parts) and len(parts) >= len(header):
            rows.append(dict(zip(header, map(int, parts[: len(header)]))))
    return rows


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("file", help="vmstat output file or - for stdin")
    ap.add_argument("--cpus", type=int, required=True, help="number of CPUs (nproc)")
    ap.add_argument("--keep-first", action="store_true", help="do not drop the since-boot sample")
    a = ap.parse_args()

    text = sys.stdin.read() if a.file == "-" else open(a.file, encoding="utf-8").read()
    rows = parse(text.splitlines())
    if not a.keep_first and len(rows) > 1:
        rows = rows[1:]
    if not rows:
        print("no vmstat samples found")
        return 2

    def col(name):
        return [r.get(name, 0) for r in rows]

    def summary(name):
        v = col(name)
        return statistics.mean(v), max(v)

    n = len(rows)
    findings = []
    r_mean, r_max = summary("r")
    b_mean, b_max = summary("b")
    si_mean, _ = summary("si")
    so_mean, _ = summary("so")
    wa_mean, wa_max = summary("wa")
    st_mean, _ = summary("st")
    us_mean, _ = summary("us")
    sy_mean, _ = summary("sy")
    id_mean, _ = summary("id")
    cs_mean, _ = summary("cs")
    swapping = sum(1 for r in rows if r.get("si", 0) + r.get("so", 0) > 0)

    if r_mean > a.cpus:
        findings.append(("CPU", f"run queue avg {r_mean:.1f} > {a.cpus} CPUs (max {r_max})", r_mean / a.cpus))
    if us_mean + sy_mean > 90:
        findings.append(("CPU", f"CPU busy {us_mean + sy_mean:.0f}% (us {us_mean:.0f} sy {sy_mean:.0f})", 1.5))
    if b_mean >= 1:
        findings.append(("DISK", f"blocked (D-state) tasks avg {b_mean:.1f} (max {b_max})", 1 + b_mean / 2))
    if wa_mean > 20:
        findings.append(("DISK", f"iowait avg {wa_mean:.0f}% (max {wa_max})", 1 + wa_mean / 40))
    if swapping > n / 3:
        findings.append(("MEMORY", f"swapping in {swapping}/{n} samples (si {si_mean:.0f} so {so_mean:.0f} per s)", 2 + so_mean / 100))
    if st_mean > 10:
        findings.append(("CPU-STEAL", f"steal avg {st_mean:.0f}%: hypervisor contention or burst credits exhausted", 1 + st_mean / 20))
    if sy_mean > 30 and sy_mean > us_mean:
        findings.append(("KERNEL", f"system time {sy_mean:.0f}% exceeds user time: syscalls, locks or interrupts", 1.2))
    if cs_mean > 50000 * a.cpus / 8:
        findings.append(("KERNEL", f"context switches avg {cs_mean:.0f}/s: thread contention likely", 1.1))

    print(f"samples: {n}  cpus: {a.cpus}")
    print(f"r {r_mean:.1f}/{r_max}  b {b_mean:.1f}/{b_max}  us {us_mean:.0f}  sy {sy_mean:.0f}  "
          f"id {id_mean:.0f}  wa {wa_mean:.0f}  st {st_mean:.0f}  si {si_mean:.0f}  so {so_mean:.0f}  cs {cs_mean:.0f}")
    if not findings:
        print("no saturation detected in this window; check application latency, locks, or downstream services")
        return 0
    findings.sort(key=lambda f: f[2], reverse=True)
    for res, msg, _ in findings:
        print(f"SATURATED {res:10} {msg}")
    print(f"dominant suspect: {findings[0][0]}")
    return 1


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc