Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Diagnose High CPU and Memory Usage on a Linux Server

By Kokil Thapa | Last reviewed: September 2026

Your monitoring alert fired at 2 a.m. Load average is climbing, pages time out, and SSH feels sluggish. You need to diagnose high CPU and memory usage on a Linux server before a reboot wipes the evidence. On production Ubuntu boxes running Apache, PHP-FPM, and MySQL, the culprit is rarely the kernel itself. It is usually a runaway worker, a stuck queue job, or a query that escaped staging. This guide walks through the exact commands and decision paths I use on real client servers.

How do you diagnose high CPU and memory usage on a Linux server?

Start with a 60-second triage loop. You want three numbers: load average, available memory, and the top five processes by CPU or RSS. Do not reboot yet. A reboot destroys the process list and often hides the root cause.

Connect over SSH and run these commands in order:

  1. uptime — shows load averages for 1, 5, and 15 minutes.
  2. free -h — shows total, used, and available memory plus swap.
  3. top -b -n 1 | head -20 — one snapshot of the heaviest processes.
  4. df -h — confirms disk is not full; a full disk causes I/O wait that looks like CPU load.

Compare load average to your CPU core count. On a 4-core VPS, sustained load above 4.0 means every core is busy. If load is high but CPU idle percentage in top is also high, suspect I/O wait—not compute saturation. That pattern often points to slow disk, a missing index, or a backup job hammering the filesystem.

Diagnose High CPU and Memory UsageAlertLoad / OOMTriageuptime / freeIdentifytop / ps PIDMapsystemd unitInspect Logs and Configjournalctl, slow query, PHP-FPM poolFix Root Causetune / patch / killPrevent Repeatmonitor + limitsGoal: fix one service, not reboot the whole VPS
Workflow to diagnose high CPU and memory usage on a Linux server without destroying forensic data

Write down the timestamp, load values, and top PIDs before you change anything. If the incident is on a Laravel app, note whether queue workers, schedulers, or web requests correlate with the spike. Persistent monitoring from tools like Netdata with alert thresholds makes this triage faster because you already know which graph moved first.

Reading load average versus CPU percentage

Load average counts runnable and uninterruptible tasks. It does not distinguish CPU-bound from disk-bound work. Press 1 in interactive top to see per-CPU usage. If wa (I/O wait) dominates, check MySQL slow logs and backup cron jobs before blaming PHP.

Understanding memory: used versus available

On modern Linux kernels, free -h shows an available column that includes reclaimable page cache. Do not panic when used looks high. Panic when available approaches zero and swap usage climbs. The kernel's proc filesystem documentation explains how /proc/meminfo fields relate to OOM behaviour.

Which Linux commands show the highest CPU and memory consumers?

Different tools answer different questions. Use the right one instead of guessing from a single top screen.

CommandBest forLimitation
top / htopLive CPU and memory rankingHard to correlate with systemd units
ps aux --sort=-%mem | headSnapshot of top memory PIDsStatic; misses short spikes
systemd-cgtopResource use per cgroup/serviceRequires systemd; less detail per thread
smem -rs pssAccurate shared memory attributionNot installed by default on minimal images
pidstat -r 1 5Memory trends per PID over timePart of sysstat package

For a quick memory snapshot sorted by resident set size:

ps aux --sort=-%mem | head -15
ps aux --sort=-%cpu | head -15

To see which systemd unit owns a process, use the PID you captured:

systemctl status <PID>
cat /proc/<PID>/cgroup

On servers I maintain with Apache and PHP-FPM 8.3 or 8.4, systemd-cgtop often shows php8.3-fpm.service or mysql.service at the top within seconds. That immediately narrows the search. For deeper service control patterns, see how systemd manages services on Linux.

Installing and using htop

htop adds tree view, colour cues, and F6 sort options. Install on Ubuntu with sudo apt install htop. Press F5 for process tree—useful when one parent spawns dozens of PHP workers. Press F6 and sort by PERCENT_MEM or PERCENT_CPU.

Capturing evidence before you kill a process

Before sending SIGKILL, dump state:

PID=12345
ps -fp $PID
cat /proc/$PID/cmdline | tr '\0' ' '; echo
ls -l /proc/$PID/fd 2>/dev/null | wc -l
sudo cat /proc/$PID/stack 2>/dev/null

The open file descriptor count matters for PHP apps with leaked connections. A worker holding hundreds of sockets often points to a database or Redis handle that never closed. Related reading: PHP memory limits and common leak patterns.

How do you trace high CPU usage to PHP-FPM, MySQL, or queue workers?

Web stacks follow predictable patterns. Map the process name to the layer, then open that layer's logs.

PHP-FPM workers stuck at 100% CPU

PHP-FPM pool processes appear as php-fpm: pool www or php8.3-fpm: pool www. When every worker is busy, new requests queue and the site hangs. Check pool status if enabled in /etc/php/8.3/fpm/pool.d/www.conf:

pm.status_path = /fpm-status
ping.path = /fpm-ping

Query status with curl from localhost (protect this path in production):

curl -s http://127.0.0.1/fpm-status?full

Look for max children reached in /var/log/php8.3-fpm.log. That means your pool is undersized or requests are too slow. Tuning guidance lives in PHP-FPM tuning for high-traffic websites and PHP-FPM configuration for high-traffic sites.

To sample what a hot PHP worker is doing, use strace briefly:

sudo strace -p <PID> -c -f -t 2>&1 | head -30

Heavy read or poll syscalls on a database socket mean a slow query, not bad PHP code. Heavy write to a log file may mean debug logging left on in production.

Process Name to Service Layerphp-fpm: poolPHP-FPMweb requestsmysqldMySQL 9.7queries / bufferredis-serverRedis 8.10cache / queuesphp artisan queueLaravel Queuebackground jobsCheck These Logs• /var/log/php8.3-fpm.log• /var/log/mysql/error.log• storage/logs/laravel.log• journalctl -u php8.3-fpm• slow query log• /var/log/syslog cron
How to trace Linux process names to PHP-FPM, MySQL, Redis, and Laravel queue workers during CPU spikes

MySQL consuming RAM or CPU

mysqld often tops memory on LAMP stacks. MySQL 9.7 and the 8.4 LTS line both cache aggressively. Check current connections and running queries:

mysql -e "SHOW FULL PROCESSLIST;"
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running';"

Enable the slow query log temporarily if you suspect bad SQL. Long-running SELECT statements with Using filesort or full table scans burn CPU and block InnoDB threads. Fix queries before buying a bigger VPS. Start with optimizing MySQL queries for high-traffic applications.

Laravel queue workers and cron overlap

Background jobs appear as php artisan queue:work or supervisor-managed processes. A stuck job retrying every second can saturate CPU across multiple workers. I've seen this on production Laravel applications when a third-party API times out but the job lacks backoff.

Inspect the queue and failed jobs table, then read scaling Laravel background jobs. Check cron with grep CRON /var/log/syslog—overlapping schedulers are a common mistake when both system cron and Laravel's scheduler fire the same export.

What causes sudden memory spikes on a Ubuntu web server?

Memory spikes usually fall into five categories. Match symptoms to the category before you change pool sizes blindly.

  • PHP-FPM pool too large: pm.max_children multiplied by per-worker RSS exceeds RAM.
  • MySQL buffer overcommit: innodb_buffer_pool_size plus connection buffers eats most of the box.
  • Cache stampede: expired keys trigger simultaneous regeneration across workers.
  • Log or backup jobs: gzip, tar, or mysqldump spiking memory during nightly cron.
  • Memory leak: worker RSS grows over hours until the OOM killer strikes.

Estimate safe PHP-FPM children with this rough formula:

available_ram_mb / average_php_worker_mb = safe max_children upper bound

Measure average worker RSS while traffic is normal:

ps -o rss= -C php-fpm8.3 | awk '{sum+=$1; n++} END {print sum/n/1024 " MB avg"}'

On a 4 GB VPS running MySQL and Redis, leaving headroom for the OS means PHP might get 1.2–1.5 GB—not 3 GB. A site like Adventure Third Pole Trek with Livewire and booking logic needs realistic pool math, not default www.conf values copied from a tutorial.

4 GB VPS Memory BudgetTotal RAM: 4096 MBMySQL ~1280 MBinnodb_buffer_poolPHP-FPM ~960max_children x RSSRedis ~256cache + sessionsOS + Apache ~512 MBkernel, page cache headroomReserve ~1088 MBspike buffer, avoid OOMOvercommit any layer → swap thrash → site timeoutUse free -h available column before adding workers
Typical memory budget on a 4 GB Linux VPS running MySQL, PHP-FPM, and Redis for a Laravel application

Detecting the OOM killer

When the kernel runs out of memory, it kills a process—often MySQL or a fat PHP worker. Check:

dmesg -T | grep -i "out of memory"
journalctl -k | grep -i oom

If OOM events appear weekly, your capacity plan is wrong or a leak exists. Adding swap on a small VPS buys time but does not fix bad pool sizing. Swap thrash feels like a CPU problem because the kernel spends cycles paging.

Redis and Memcached memory caps

Redis 8.10 defaults can grow until maxmemory is hit. Check:

redis-cli INFO memory | grep used_memory_human
redis-cli CONFIG GET maxmemory

Set an explicit maxmemory and eviction policy so cache does not consume the entire box. For cache architecture decisions, read caching strategies for high-traffic sites.

How do you fix runaway processes without rebooting the server?

Rebooting is a last resort. It clears opcache, drops established DB connections, and interrupts in-flight payments. Fix the specific offender instead.

Graceful service restart order

On a typical PHP stack, restart in this order:

  1. Stop the bleeding: rate-limit traffic at the firewall or CDN if needed.
  2. Restart PHP-FPM: sudo systemctl reload php8.3-fpm (graceful) or restart if workers are stuck.
  3. Identify and kill long MySQL queries: KILL <id>; after confirming with SHOW PROCESSLIST.
  4. Restart queue workers via Supervisor: sudo supervisorctl restart all.
  5. Only then consider MySQL restart during a maintenance window.

The systemctl manual documents the difference between reload and restart. PHP-FPM reload spawns new workers and retires old ones—prefer it during business hours on Nepali e-commerce sites where downtime costs real revenue.

Setting temporary resource limits

Use systemd drop-in overrides to cap a runaway service while you debug:

sudo systemctl edit php8.3-fpm

Add:

[Service]
MemoryMax=1500M
CPUQuota=200%

Then sudo systemctl daemon-reload && sudo systemctl restart php8.3-fpm. This prevents one pool from taking the entire machine. Remove the limit after you fix the underlying query or code path.

When killing a single PID is correct

If one PHP worker is pegged at 100% CPU for 10+ minutes while siblings are idle, kill that PID:

sudo kill -15 <PID>

PHP-FPM master will spawn a replacement. Use SIGTERM first. Reserve SIGKILL for workers that ignore graceful shutdown. Document the PID and request URL if you can trace it from access logs.

Fix Without Reboot?One bad PID?Yeskill -15 PIDNoWhole service?reload FPMOOM in dmesg?fix pool sizingReboot last resortafter capturing logs + PIDs
Decision tree to resolve high CPU and memory on Linux without a full server reboot

After stabilizing, schedule proper fixes: query indexes, queue backoff, PHP-FPM pool tuning, or a scheduled upgrade window. For ongoing incidents on busy storefronts, see hosting a high-traffic Nepali e-commerce site.

How do you prevent repeat CPU and memory incidents?

Diagnosis without prevention means you will be awake at 2 a.m. again next month. Build three layers: monitoring, alerting, and capacity headroom.

Monitoring and alert thresholds

Set alerts on load average, available memory percentage, swap usage, and PHP-FPM max children reached count. A load alert at 1.5× core count gives you time to act before customers notice. Combine Netdata or Nagios with the practices in Ubuntu server monitoring guide and Nagios monitoring for servers.

Log rotation and structured exports

Runaway disk from verbose logging causes I/O wait spikes. Ensure logrotate is configured for PHP, Apache, and MySQL logs. When exporting JSON log snippets for analysis, a JSON formatter helps validate structure before you paste into tickets or CI pipelines.

Security and abuse traffic

CPU spikes from brute-force login attempts or scraper floods look like application bugs. Check Apache access logs for repeated 404s or POST floods to wp-login.php. Pair resource monitoring with website and server security practices in Nepal and baseline hardening from Ubuntu web server hardening.

Post-incident checklist

After every spike, document:

  • Start and end time, peak load, and peak memory.
  • Top three PIDs and their systemd units.
  • Whether OOM killer fired.
  • Code deploy, cron change, or traffic event that correlated.
  • Permanent fix applied and who owns follow-up.

For teams without in-house ops capacity, support and maintenance or speed optimization engagements often start with exactly this incident log. A proper Ubuntu server setup for PHP apps done right the first time prevents many of these fires.

Key Takeaways

  • Run uptime, free -h, and top before killing processes—capture PIDs and timestamps first.
  • Map each hot PID to a systemd unit; PHP-FPM, MySQL, Redis, and queue workers have different log paths.
  • High load with high I/O wait points to disk or database problems, not always CPU-bound PHP code.
  • Size PHP-FPM max_children from measured worker RSS, not guesswork—leave RAM for MySQL and the OS.
  • Prefer systemctl reload php8.3-fpm and targeted KILL queries over a full reboot.
  • Install monitoring with alerts on load, available memory, and FPM pool exhaustion to catch the next spike early.

People Also Ask

What is a dangerous load average on a Linux server?

Sustained load above your CPU core count is a warning sign on a web server. Brief spikes during backups may be acceptable. If load stays high for 5+ minutes while response times degrade, treat it as an incident and start triage immediately.

How much swap should a web server use?

Ideally none during normal operation. Small amounts of swap use during off-peak jobs can be fine. Heavy, continuous swap in and out means RAM is undersized or a service is overcommitted—fix sizing before adding more swap space.

Can a single bad MySQL query crash the server?

A single unindexed query on a large table can peg CPU and block other connections. It rarely crashes the kernel, but it can exhaust the connection pool and PHP-FPM workers waiting on DB responses. Kill the query ID after confirming it in SHOW PROCESSLIST.

Should I install more RAM or optimize the application first?

Optimize first if memory grew suddenly after a deploy or config change. Add RAM if baseline usage legitimately increased with traffic and tuning is already sane. Buying a bigger VPS without fixing a leak only delays the next OOM event.

Next steps after you diagnose the spike

Knowing how to diagnose high CPU and memory usage on a Linux server turns a panic reboot into a 15-minute fix. Capture evidence, identify the service layer, read the right logs, and apply a targeted restart or config change. Then close the loop with monitoring and pool sizing so the same failure mode cannot silently return.

If your production stack runs Laravel, WooCommerce, or custom PHP on Ubuntu and you want someone who handles both the app and the box, review the portfolio or reach out via contact us for a structured health check. For background on baseline server layout, start with Ubuntu server setup guide and keep about me handy if you want to know who will actually SSH in at 2 a.m.

Frequently Asked Questions

Start a 60-second triage loop over SSH without rebooting. Run uptime for load averages, free -h for available memory and swap, top -b -n 1 | head -20 for the heaviest processes, and df -h to rule out a full disk causing I/O wait. Write down timestamps, load values, and top PIDs before changing anything. Compare load average to your CPU core count—on a 4-core VPS, sustained load above 4.0 means every core is busy. Map hot PIDs to systemd units with systemctl status before inspecting service logs.

Sustained load above your CPU core count is a warning sign. Brief spikes during backups may be acceptable; load above core count for 5+ minutes with degraded response times is an incident.

Use top or htop for live CPU and memory ranking, ps aux --sort=-%mem | head and ps aux --sort=-%cpu | head for static snapshots, systemd-cgtop to see resource use per cgroup or service on systemd servers, smem -rs pss for accurate shared memory attribution if installed, and pidstat -r 1 5 from the sysstat package for memory trends per PID over time. On Apache and PHP-FPM stacks, systemd-cgtop often surfaces php8.3-fpm.service or mysql.service within seconds. Map any PID to its owning unit with systemctl status PID and cat /proc/PID/cgroup.

Load average counts runnable and uninterruptible tasks—it does not distinguish CPU-bound work from disk-bound work. Press 1 in interactive top to see per-CPU usage. If wa (I/O wait) dominates while load is high, suspect slow disk, a missing MySQL index, or a backup job hammering the filesystem rather than PHP compute saturation. That pattern often means SHOW PROCESSLIST and slow query logs deserve attention before you blame application code or scale the VPS.

Ideally none during normal operation. Small swap use during off-peak jobs can be fine. Heavy, continuous swap in and out means RAM is undersized or a service is overcommitted.

Map process names to layers. PHP-FPM workers appear as php-fpm: pool www or php8.3-fpm: pool www—curl the fpm-status endpoint if enabled and check /var/log/php8.3-fpm.log for max children reached. For MySQL, run SHOW FULL PROCESSLIST and SHOW GLOBAL STATUS LIKE 'Threads_running'; enable the slow query log if needed. Queue workers show as php artisan queue:work or Supervisor-managed processes; inspect queue and failed_jobs tables. strace -p PID briefly—heavy read or poll on a database socket usually means slow SQL, not bad PHP.

Five common categories: PHP-FPM pm.max_children multiplied by per-worker RSS exceeds RAM; MySQL overcommit from innodb_buffer_pool_size plus connection buffers; cache stampede when expired keys regenerate simultaneously; nightly cron jobs such as gzip, tar, or mysqldump; and memory leaks where worker RSS grows until the OOM killer fires. Estimate safe max_children as available_ram_mb divided by average_php_worker_mb. On a 4 GB VPS with MySQL and Redis, PHP might realistically get 1.2–1.5 GB—not the full box.

Check dmesg -T | grep -i "out of memory" and journalctl -k | grep -i oom. Weekly OOM events signal wrong capacity planning or a leak; swap only buys time.

A single unindexed query on a large table can peg CPU and block other connections through InnoDB threads. It rarely crashes the kernel, but it can exhaust the connection pool and leave PHP-FPM workers waiting on database responses until the site hangs. Confirm the offender with SHOW FULL PROCESSLIST, then KILL the query ID after verifying it. Fix the SQL and indexes before upgrading hardware—a bigger VPS does not fix a full table scan.

Rebooting drops opcache, DB connections, and in-flight payments—avoid it during triage. Stop the bleeding with firewall or CDN rate limits, then systemctl reload php8.3-fpm for graceful worker retirement. Kill long MySQL queries after SHOW PROCESSLIST confirmation. Restart queue workers via supervisorctl restart all. Use systemd drop-in MemoryMax and CPUQuota caps on php8.3-fpm while debugging. For one worker pegged at 100% CPU for 10+ minutes while siblings are idle, sudo kill -15 PID first—PHP-FPM spawns a replacement. Reserve SIGKILL for workers ignoring graceful shutdown.

Measure average worker RSS during normal traffic: ps -o rss= -C php-fpm8.3 | awk '{sum+=$1; n++} END {print sum/n/1024 " MB avg"}'. Divide available RAM (after leaving headroom for MySQL, Redis, and the OS) by that average. On a 4 GB VPS running MySQL and Redis, PHP might get 1.2–1.5 GB total—not 3 GB. Copying default www.conf values from tutorials without this math is a common cause of OOM events on Laravel and Livewire apps.

Load average includes uninterruptible tasks waiting on disk I/O, not just CPU work. When top shows high load but most cores are idle with wa (I/O wait) dominating, the bottleneck is disk or database I/O—slow queries, missing indexes, backup jobs, or a full filesystem from unrotated logs. Running df -h during triage confirms whether disk space is the trigger. Fix MySQL slow logs and backup cron overlap before blaming PHP-FPM pool sizes or purchasing a larger CPU tier.

No—reboot first destroys forensic evidence. The process list, hot PIDs, and correlation with deploys or cron jobs disappear after reboot, often hiding the root cause until the next 2 a.m. alert. Run uptime, free -h, and top, capture PIDs and timestamps, map processes to systemd units, inspect logs, then reload or restart the specific offender. Treat full reboot as a last resort after targeted fixes fail or the kernel itself is unresponsive.

Build three layers: monitoring, alerting, and capacity headroom. Alert on load average at roughly 1.5× core count, available memory percentage, swap usage, and PHP-FPM max children reached. Tools like Netdata or Nagios accelerate triage when graphs show which metric moved first. Ensure logrotate covers PHP, Apache, and MySQL logs—runaway verbose logging causes I/O wait spikes. Check access logs for brute-force or scraper floods that mimic application bugs. After each incident, document timestamps, peak load, top PIDs, OOM events, correlated deploys, and the permanent fix owner.

Use a drop-in override while debugging: sudo systemctl edit php8.3-fpm, add MemoryMax=1500M and CPUQuota=200% under [Service], then daemon-reload and restart the unit. This stops one pool from consuming the entire machine while you fix the underlying slow query or code path. Remove the cap after the fix is verified. Prefer systemctl reload over restart during business hours on live storefronts—reload spawns new workers and retires old ones without dropping every in-flight request at once.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: