Linux Performance Troubleshooting: A Real Incident Walkthrough
Step-by-step debugging of a production Linux server hitting 100% CPU. From top to perf to the actual fix.
Key takeaways
- Step-by-step debugging of a production Linux server hitting 100% CPU.
- From top to perf to the actual fix.
On this page
Linux Performance Troubleshooting: A Real Incident Walkthrough#
Last month our API servers started responding in 8 seconds instead of 200ms. Here's exactly how we diagnosed and fixed it using standard Linux tools.
Step 1: top - Get the Big Picture#
top -bn1 | head -20
Output showed:
- CPU: 98% user, 1% system, 1% idle
- One process (
node) consuming 94% CPU - Load average: 12.4 on a 4-core machine
Step 2: Identify the Process#
ps aux --sort=-%cpu | head -5
The culprit was our Node.js API process, PID 28431.
Step 3: strace - What's It Doing?#
strace -c -p 28431 -e trace=read,write,futex
Results: 89% of syscalls were futex (lock contention) and read from a file descriptor.
Step 4: Check File Descriptors#
ls -la /proc/28431/fd | wc -l
# Result: 4,847
lsof -p 28431 | grep -c "TCP"
# Result: 4,201 TCP connections
We had 4,201 open TCP connections. Our connection pool had no limit, and a downstream service was responding slowly, causing connections to pile up.
Step 5: Confirm with ss#
ss -tnp | grep 28431 | awk '{print $4}' | sort | uniq -c | sort -rn | head
Result: 3,800 connections to port 5432 (PostgreSQL) in ESTABLISHED state. The database wasn't the bottleneck—our pool was creating connections faster than queries completed.
The Fix#
- Added connection pool limits in our database client:
const pool = new Pool({
max: 20, // was unlimited
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
- Added a circuit breaker for the slow downstream service
- Set up alerts for open file descriptor count:
# /etc/prometheus/alerts/fd_alert.yml
- alert: HighFileDescriptors
expr: process_open_fds > 1000
for: 5m
labels:
severity: warning
Best Practices for Linux Performance#
- Start with
top, then drill down withps,strace,lsof - Check file descriptors when CPU is high—connection leaks are a common cause
- Set limits on everything: connection pools, file descriptors (
ulimit), memory - Monitor proactively: track open FDs, TCP connections, and load average
- Reproduce in staging before making production changes when possible
The issue wasn't a code bug in the traditional sense—it was a missing configuration. Default "unlimited" settings are the cause of more outages than most teams realize.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Prompt Engineering Patterns That Actually Work in Production
Battle-tested prompt patterns from running LLM features in production: structured output, chain-of-thought, and graceful failure handling.
Terraform Modules Done Right: Lessons from Managing 50+ Services
Practical patterns for Terraform modules at scale: versioning, composition, testing, and avoiding the monolith trap.
More from Linux
Explore more articles in this category
ext4 vs XFS vs Btrfs: Choosing a Filesystem for a Server
The default filesystem your distro picks is not always the right one for your workload. Here is what actually differs and when each one wins.
journald Log Management: Retention, Filtering, and Forwarding
journald is the default log sink on every systemd distro, and most of it runs on defaults nobody chose. Here is how to actually control it.
DNS Troubleshooting on Linux: A Systematic Approach
\"It's always DNS\" is a joke because the failure modes are so scattered: resolver config, caching, search domains, split DNS. Here is where to actually look.
You might have missed
Evergreen posts worth revisiting.