How to Troubleshoot a Linux Production Server
The exact sequence of Linux commands to run when a production server is degraded — CPU, memory, disk, network, logs, and a real incident walkthrough.
At 2am an alert fires. Your payment API is timing out. Users are seeing 503 errors. You SSH into the server and open top.
Most engineers stare at it, pick the process with the highest CPU, and guess. That approach takes 20 minutes. This one takes 4 — because it follows a fixed decision tree instead of improvising under pressure.
The 7-Step Diagnostic Sequence
Work through these steps in order. Every check narrows the problem. Stop at the step where you find the culprit — you rarely need to reach step 7.
Step 1: System Snapshot (30 seconds)Step 2: CPU (check if saturated)Step 3: Memory (check for OOM or swap)Step 4: Disk (space, inodes, I/O wait)Step 5: Network (connections, port state)Step 6: Logs (find the exact error)Step 7: Recent Changes (what changed 2 hours ago)Step 1 — Get the System Snapshot
Run these four commands immediately. They give you the full picture in 30 seconds.
uptimefree -hdf -hss -tulpn | wc -luptime shows the load average — three numbers representing 1, 5, and 15-minute windows.
10:01:23 up 14 days, load average: 8.42, 4.11, 2.05A load average above your CPU core count means the system is overloaded. On a 4-core server, 8.42 means twice as many processes are waiting to run as the CPU can service. Run nproc to confirm your core count.
free -h reveals memory pressure. Watch available, not free — when available drops below 10% of total RAM, the kernel begins swapping aggressively to disk. That is catastrophic for databases and APIs.
df -h catches a full disk before you waste 10 minutes elsewhere. A full disk causes processes to crash silently with no obvious log trail.
Step 2 — Diagnose CPU Saturation
If load average is above core count, find what is consuming it.
ps aux --sort=-%cpu | head -15## Check the iowait percentagetop -bn1 | grep "Cpu(s)"The %CPU column shows the culprit immediately. At Swiggy, a misconfigured background job consuming 97% CPU caused the order API to queue requests for 8 seconds during a dinner spike — ps aux found it in 15 seconds.
Reading the CPU output pattern:
| Pattern | What it means |
|---|---|
| One process at 90%+ | Code regression — infinite loop or O(n^2) algorithm |
| Many processes at 10-30% | Traffic spike — scale horizontally |
kswapd high |
Memory pressure forcing page swaps |
| iowait above 20% | Disk bottleneck, not CPU — skip to Step 4 |
The iowait percentage from top is the most commonly missed signal. If %wa is above 20, the CPU is waiting on disk. Tuning the application will not help — you need to address disk I/O first.
For historical trend context beyond the current snapshot, sar -u 1 5 (from the sysstat package) shows CPU behavior over the last few minutes rather than a single instant — useful for confirming whether a spike is sustained or already passing.
Step 3 — Diagnose Memory and OOM Events
free -hdmesg | grep -i "oom\|killed" | tail -20cat /proc/meminfo | grep -E "MemAvailable|SwapUsed"When the kernel runs out of memory it invokes the OOM killer — it terminates the process with the highest memory score to reclaim RAM. The victim is almost never the process you would expect.
A real OOM entry in dmesg looks like this:
Out of memory: Kill process 1234 (node) score 821 or sacrifice childKilled process 1234 (node) total-vm:2048MB, anon-rss:1834MBThe score 821 indicates how aggressively the kernel targeted that process — higher scores get killed first. When you see this at Zerodha for example, it is almost always the analytics aggregation job — not the trading engine — consuming memory unnoticed until it starves everything else.
To find which processes are currently consuming swap:
for pid in $(ls /proc | grep -E '^[0-9]+$'); do swap=$(grep VmSwap /proc/$pid/status 2>/dev/null \ | awk '{print $2}') [ "$swap" -gt "0" ] 2>/dev/null \ && echo "PID $pid: ${swap}kB"done | sort -t: -k2 -rn | head -10Step 4 — Diagnose Disk Issues
A full disk is the sneakiest failure mode. Processes write nothing, log nothing, and silently return errors that look like application bugs.
## Space usage per mountdf -h## Inode usage per mountdf -ih## I/O wait and utilisation, 3 samplesiostat -xz 1 3The inode trap is frequently missed. You can have 40GB of free disk space and zero inodes remaining. Every open() system call will fail with ENOSPC — the same error as a full disk. The difference only appears in df -ih.
To locate the directory generating the most files:
find / -xdev -printf '%h\n' 2>/dev/null \ | sort | uniq -c | sort -rn | head -10For iostat, watch the %util column — above 80% means the disk is saturated. The await column shows average I/O wait time in milliseconds. Above 20ms for SSDs or 100ms for spinning disks indicates queueing that will slow every process touching that device.
Step 5 — Diagnose Network and Connections
Networking failures masquerade as application failures. What looks like a hanging API is often a connection that cannot be established.
## All listeners with process namesss -tulpn## Connection counts by statess -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn## Total established connectionsss -ant | grep ESTABLISHED | wc -lConnection state guide:
| State | What it means |
|---|---|
TIME_WAIT high |
Normal after traffic spike — connections closing gracefully |
CLOSE_WAIT high |
Application bug — not closing connections after use |
SYN_RECV high |
TCP SYN flood or upstream connection leak |
Test connectivity to a specific dependency directly from the server:
timeout 3 bash -c \ 'cat < /dev/null > /dev/tcp/db.internal.razorpay.net/5432' \ && echo "DB port open" || echo "DB unreachable" dig payments-db.internal.razorpay.net +shortThis isolates whether the problem is application-level or infrastructure-level in one command. If the TCP connection fails, the application cannot fix it — you have a network, firewall, or service issue.
Step 6 — Read the Logs
By step 6 you know which component is under pressure. Now find the specific error that triggered the incident.
journalctl -u payment-api -n 100 --no-pagerjournalctl -u payment-api --since "30 minutes ago"journalctl -p err -b --no-pager | tail -30grep -i "error\|exception\|fatal" \ /var/log/app/payment-api.log | tail -50dmesg | tail -30The most important question is: when did the first error appear?
grep -n "ERROR" /var/log/app/payment-api.log | head -3The timestamp of the first error tells you where to look for the cause. What changed in the 2-5 minutes before that line?
Step 7 — Find What Changed
Eight out of ten production incidents trace back to a change in the last 2 hours. Always end your investigation by confirming what changed.
find /etc /opt /app -mmin -120 -type f 2>/dev/nulljournalctl --since "2 hours ago" \ | grep -E "Started|Stopped|Failed" | head -20grep " install \| upgrade " \ /var/log/dpkg.log | tail -10If the server runs a cron job, check whether it fired recently:
grep "CRON" /var/log/syslog \ | grep "$(date --date='1 hour ago' '+%b %e %H')" \ | head -10A Real Incident: Razorpay Checkout Latency
Symptom: Checkout API p99 latency spiked from 180ms to 4200ms at 01:47. Payment failure rate rose to 12%.
## Step 1uptime## load average: 12.45, 8.31, 4.22 on a 4-core host ## Step 2ps aux --sort=-%cpu | head -5## postgres 3421 97.3 12.1 autovacuum worker ## Step 6journalctl -u postgresql --since "1 hour ago" \ | grep -i "autovacuum\|lock"## LOG: autovacuum: processing table "payments" (48 GB)## LOG: process 3421 acquired lock on relation "payments"Root cause: PostgreSQL autovacuum acquired a lock on the 48GB payments table during peak checkout traffic. Every query to that table queued behind the lock for 3 minutes 40 seconds.
Total investigation time: 4 minutes. The fix — cancelling the autovacuum and scheduling it for off-peak hours — took 2 minutes.
The Full Diagnostic One-Liner Sequence
Copy this block when an alert fires. Run it top to bottom.
## snapshotuptime && free -h && df -h## cpups aux --sort=-%cpu | head -10## memorydmesg | grep -i oom | tail -5## diskdf -ih && iostat -xz 1 2## networkss -tulpn | head -20## logsjournalctl -p err -b --no-pager | tail -30## changesfind /etc /opt -mmin -120 -type f 2>/dev/nullProduction Implementation Guidelines
Run this sequence on every P1 alert before escalating. Systematic diagnosis reduces MTTR from 20-30 minutes to under 5 minutes — the difference between one missed SLO and ten.
For teams running on Indian cloud infrastructure, iostat is especially relevant on gp2 EBS volumes in ap-south-1 — the Mumbai region's gp2 IOPS burst bucket depletes quickly under sustained write loads, causing iowait spikes that are invisible to application-level monitoring.
Configure a motd or alias for this sequence on every production bastion host so any engineer can run it without looking it up during an incident. Print or pin the decision tree from Step 1-7 somewhere visible — the value of a systematic sequence is that a junior engineer can follow it correctly at 2am without having memorized it first.
NoteReferences and Further Reading
- Linux Performance Analysis in 60 Seconds — Netflix Tech Blog
- USE Method by Brendan Gregg — Utilisation, Saturation, Errors framework
- systemd journalctl Manual — Official reference
Frequently Asked Questions
What is the single most commonly missed signal in Linux server troubleshooting?
Inode exhaustion. df -h can show 40GB free while df -ih shows 0% inodes remaining — every open() call then fails with the same ENOSPC error as a genuinely full disk, sending engineers looking in the wrong place.
How do I tell if high CPU is a code problem or a traffic problem?
Check the process distribution, not just the load average. One process pinned at 90%+ is almost always a code regression (infinite loop, runaway query). Many processes each at 10-30% usually means real traffic growth that needs horizontal scaling, not a code fix.
Why does iowait matter more than raw CPU percentage during an incident?
If %wa from top is above 20%, the CPU is idle-waiting on disk, not actually saturated. Tuning application code will not fix a disk I/O bottleneck — you need to address storage throughput or the underlying volume type first.
How do you distinguish a network problem from an application bug during an incident?
Test connectivity to the specific dependency directly from the server with a raw TCP check against the target host and port — if the TCP connection itself fails, the problem is network, firewall, or service-level, and no amount of application-level debugging will fix it.
What percentage of production incidents trace back to a recent change?
Roughly eight out of ten — which is why the last diagnostic step should always be checking what changed in the preceding two hours (recently modified files, recent deployments, recent package installs) rather than assuming the cause is unrelated to any recent action.
Discussion0