Back to tutorials
Tutorial

VPS log analysis tutorial (2026): Find 500 errors, slow requests, and bot spikes using Nginx/Apache logs

VPS log analysis tutorial for Nginx/Apache: pinpoint 500 errors, slow URLs, bot floods, and top IPs with practical commands.

By Anurag Singh
Updated on Sep 16, 2026
Category: Tutorial
Share article
VPS log analysis tutorial (2026): Find 500 errors, slow requests, and bot spikes using Nginx/Apache logs

A “slow site” report rarely starts as a CPU problem. In 2026, most VPS incidents appear in logs before they show up in graphs. Look for one URL throwing 500s, a dead upstream, a bot spike hammering wp-login.php, or PHP workers piling up. This VPS log analysis tutorial gives you a repeatable workflow using SSH and standard Linux tools.

You’ll get to answers fast:

  • What is failing (status codes, endpoints)
  • Where it’s coming from (URL/IP/user agent)
  • Why it’s happening (app, web server, TLS, disk, permissions)

The commands below work on Ubuntu 24.04/26.04 LTS, Debian 12/13, AlmaLinux 9/10, and Rocky Linux 9/10.

What you need before you start

  • SSH access as root (or sudo) to your VPS/dedicated server
  • Knowledge of whether you’re using Nginx, Apache, or a control panel stack (cPanel/WHM, Plesk, DirectAdmin)
  • 10 minutes to collect evidence before you change anything

If you host client sites or multiple projects, aim for a staging window or a quiet maintenance slot. On production, get a clean baseline first. Then make targeted changes.

Running a busy stack? A HostMyCode VPS gives you dedicated resources. That makes it easier to keep logs longer, rotate safely, and troubleshoot without fighting noisy neighbors.

Step 1: Locate the right logs (common paths)

Start by confirming what’s actually serving requests. On many VPS setups, Nginx fronts Apache. That can mean two sets of logs and two different failure modes.

Nginx (typical paths)

  • /var/log/nginx/access.log
  • /var/log/nginx/error.log
  • Per-site logs often in /var/log/nginx/ as example.com.access.log

Apache (typical paths)

  • Debian/Ubuntu: /var/log/apache2/access.log, /var/log/apache2/error.log
  • RHEL-family: /var/log/httpd/access_log, /var/log/httpd/error_log
  • Per-vhost logs may live under /var/log/apache2/ or a custom CustomLog path

cPanel/WHM quick pointers

  • Apache domlogs: /usr/local/apache/domlogs/
  • Main Apache error log: /usr/local/apache/logs/error_log
  • Exim mail (separate topic): /var/log/exim_mainlog

Not sure which stack you’re on? Run:

sudo ss -ltnp | egrep ':(80|443)'
ps aux | egrep 'nginx|apache2|httpd' | grep -v egrep

If you’re on cPanel and want the security side covered too, keep this companion guide handy: cPanel hardening steps for WHM and hosted accounts.

Step 2: Confirm the incident window (avoid chasing old noise)

Logs usually rotate daily. If the problem started “about an hour ago,” use the active file. Don’t start with yesterday’s .1 or a compressed .gz.

Check which files were written most recently:

sudo ls -lh /var/log/nginx/ /var/log/apache2/ 2>/dev/null | tail -n 30
sudo stat /var/log/nginx/access.log 2>/dev/null | egrep 'Modify|Change'

Then tail live while you reproduce the issue in a browser:

# Nginx
sudo tail -f /var/log/nginx/access.log

# Apache
sudo tail -f /var/log/apache2/error.log

Tip: if you’re testing from your own IP, filter to your traffic. That makes cause and effect obvious.

MYIP="203.0.113.50"
sudo tail -f /var/log/nginx/access.log | grep "$MYIP"

Step 3: Identify 500 errors fast (URLs, counts, and patterns)

HTTP 500/502/503 errors are often the quickest path to the root cause. They commonly map to backend failures. Think PHP-FPM crashes, upstream timeouts, permissions problems, missing files, or OOM kills.

Nginx: top failing URLs by status code

This assumes a standard combined log format where status is the 9th field. If your format differs, adjust the field numbers.

# Top 500s
sudo awk '$9 ~ /^5[0-9][0-9]$/ {print $7}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -nr | head -n 20

# Separate gateway vs app failures
for code in 500 502 503 504; do
  echo "== $code ==";
  sudo awk -v c="$code" '$9==c {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -n 10;
done

Apache: top 500s (common log format)

sudo awk '$9 ~ /^5[0-9][0-9]$/ {print $7}' /var/log/apache2/access.log \
  | sort | uniq -c | sort -nr | head -n 20

Next move: jump to the matching error log

Access logs tell you what failed. Error logs usually tell you why it failed.

# Nginx error log (last 200 lines)
sudo tail -n 200 /var/log/nginx/error.log

# Apache error log
sudo tail -n 200 /var/log/apache2/error.log

Common “why” lines you’ll recognize:

  • Nginx: upstream prematurely closed connection (app crash)
  • Nginx: connect() failed (111: Connection refused) while connecting to upstream (PHP-FPM/downstream dead)
  • Apache: Permission denied or File does not exist (ownership/path problems)
  • PHP: Allowed memory size exhausted (code or limits)

Step 4: Spot slow requests (no APM required)

You can get solid latency signals from plain web server logs. If your Nginx log includes $request_time and $upstream_response_time, you can rank slow endpoints quickly. That beats guessing.

Check whether your Nginx log contains timing fields

Open your vhost or main config and look for log_format:

sudo nginx -T 2>/dev/null | sed -n '1,200p' | grep -n "log_format" -n

If you see request_time in the format, you can extract it. Example: assume the last field is request_time. If not, align the field selection with your log format first.

Top slow URLs (Nginx with request_time as last field)

# Show requests taking > 2 seconds
sudo awk '$(NF) > 2 {print $(NF), $7, $9}' /var/log/nginx/access.log \
  | sort -nr | head -n 50

No timing fields and you can’t change the format right now? You can still narrow it down with proxies and symptoms.

  • Spikes in 499 (client closed request) often track upstream slowness
  • 502/504 usually point to timeouts between Nginx and the app layer
  • Repeated hits to one dynamic URL can exhaust PHP workers fast

On WordPress stacks, PHP-FPM tuning is often where the biggest wins come from. If you’re on cPanel, see: PHP-FPM setup for faster WordPress on a cPanel VPS.

Step 5: Detect bot floods and abusive IPs (top talkers)

Don’t block first and ask questions later. Confirm whether it’s an actual attack or normal crawl traffic. The patterns that matter are high request volume, suspicious paths, and repeated 404/401/403 responses.

Top IPs by request count

# Nginx/Apache (IP is usually field 1)
sudo awk '{print $1}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -nr | head -n 30

Top requested paths (helps identify the target)

sudo awk '{print $7}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -nr | head -n 30

Find wp-login.php and xmlrpc hits (WordPress)

sudo awk '$7 ~ /wp-login\.php|xmlrpc\.php/ {print $1, $7, $9}' /var/log/nginx/access.log \
  | head -n 100

If it’s brute-force or low-quality bot traffic, rate limiting at the edge is usually cleaner than piling on plugins. This pairs well with: Nginx rate limiting for WordPress login abuse.

Step 6: Tie web errors to system events (OOM, disk full, permissions)

Many “mysterious” 500s have boring causes. The kernel killed workers, the disk filled, or a deploy changed ownership.

Check for OOM kills and service crashes

# Kernel OOM messages
sudo journalctl -k --since "2 hours ago" | egrep -i 'oom|killed process|out of memory'

# PHP-FPM and Nginx/Apache service logs
sudo journalctl -u php8.3-fpm --since "2 hours ago" --no-pager | tail -n 200
sudo journalctl -u nginx --since "2 hours ago" --no-pager | tail -n 200
sudo journalctl -u apache2 --since "2 hours ago" --no-pager | tail -n 200

Confirm you aren’t out of disk or inodes

df -h
df -i
sudo du -xhd1 /var | sort -h

If you’re seeing “No space left on device” or logs suddenly stop writing, use a focused checklist: find what’s filling /var and fix disk pressure.

Step 7: Use grep/jq-style filtering for one domain on a multi-site host

On a multi-site VPS, “check the logs” is too broad. You usually need one domain, one vhost, or one account. Otherwise, the signal gets buried.

Nginx: isolate by Host header (if logged) or by per-site logs

If you have per-site logs, use them first. If you don’t, and your log format includes $host, filter by the domain.

DOMAIN="example.com"
# If $host is present in the line
sudo grep -F " $DOMAIN " /var/log/nginx/access.log | tail -n 50

Apache: isolate by vhost log files

Many Apache setups already log per-vhost. If you see files like example.com-access.log, work there.

sudo ls -1 /var/log/apache2/ | grep -i example

Step 8: Quick TLS/HTTPS failure checks from logs

TLS failures don’t always reach the access log. Often the request never completes. In that case, your best signal is the Nginx error log plus what the client reports.

Look for handshake and certificate problems:

sudo egrep -i 'ssl|tls|handshake|certificate|alert' /var/log/nginx/error.log | tail -n 80

If you suspect weak ciphers, HSTS mistakes, or chain issues, keep your HTTPS baseline consistent. This guide is the practical reference: TLS hardening for Nginx or Apache.

Step 9: Make logs sustainable (rotation and retention without disk spikes)

Log analysis only helps if the logs still exist when you need them. But “keep everything forever” is how VPS disks fill up quietly.

Confirm logrotate is installed and active:

sudo systemctl status logrotate.timer 2>/dev/null || true
sudo ls -l /etc/logrotate.d/

Then inspect the web server logrotate configs:

# Nginx
sudo sed -n '1,200p' /etc/logrotate.d/nginx 2>/dev/null || true

# Apache
sudo sed -n '1,200p' /etc/logrotate.d/apache2 2>/dev/null || true

If you need to tune rotation (compress, delaycompress, dateext, retain 14–30 days), do it in a way that won’t trigger log bursts or reload loops: rotate and retain Nginx/Apache/PHP logs without disk spikes.

Step 10: A repeatable incident workflow (printable checklist)

  • Freeze the window: note the time the issue started and which domain(s) are affected.
  • Confirm the serving layer: Nginx vs Apache vs both.
  • Count failures: top 5xx URLs, then check error logs for the same time window.
  • Check resource killers: OOM, disk full, inode exhaustion, service restarts.
  • Identify abuse: top IPs, targeted paths, suspicious user agents.
  • Apply the smallest safe fix: restart a crashed upstream, raise a limit, block/rate limit an IP, correct permissions.
  • Verify: tail logs while reproducing and confirm status codes normalize.
  • Prevent repeat: rotate logs, add basic monitoring/alerts, set WAF/rate limits where needed.

Practical fixes you can apply immediately (without guessing)

This isn’t a full performance tuning guide. It’s a short list of low-risk fixes that often ends an incident cleanly.

Restart the right service (only after you confirm it’s unhealthy)

# Nginx
sudo systemctl restart nginx

# Apache
sudo systemctl restart apache2 2>/dev/null || sudo systemctl restart httpd

# PHP-FPM (adjust version)
sudo systemctl restart php8.3-fpm 2>/dev/null || sudo systemctl restart php-fpm

Block a clearly abusive IP (temporary emergency measure)

On Ubuntu with UFW:

ABUSER="198.51.100.23"
sudo ufw status
sudo ufw deny from "$ABUSER" to any port 80
sudo ufw deny from "$ABUSER" to any port 443

If you’re using CSF/LFD in cPanel, do it there instead. That keeps the panel as the source of truth.

Fix a common WordPress permission failure (uploads)

If the logs show permission denied for wp-content/uploads after a migration or deploy, set ownership back to your site user (example: siteuser). Be careful on multi-tenant servers.

cd /var/www/example.com
sudo chown -R siteuser:www-data wp-content/uploads
sudo find wp-content/uploads -type d -exec chmod 0755 {} \;
sudo find wp-content/uploads -type f -exec chmod 0644 {} \;

If you end up doing this kind of troubleshooting more than once a quarter, start with infrastructure that supports operations work. Use managed VPS hosting for a cleaner baseline, or a HostMyCode VPS if you want full control over logging, tuning, and incident response.

FAQ

How far back should I keep web server logs on a VPS?

For most small-to-mid hosting workloads, 14–30 days is a solid default. Keep more only if you have compliance requirements and enough disk, or if you ship logs off-host.

My access log doesn’t show request time. What should I do?

Add $request_time and (if applicable) $upstream_response_time to your Nginx log_format, then reload Nginx. Make the change during a low-traffic window and confirm any log parsers you use can handle the new format.

Why do I see lots of 499 status codes in Nginx?

499 means the client closed the connection. It often appears during slow upstream responses, mobile network drops, or bots with short timeouts. Correlate it with slow URLs and upstream errors.

Should I analyze logs on the server or ship them elsewhere?

On-server analysis is usually fastest during an incident. Shipping logs off-host helps for long-term trends, but many VPS setups don’t need it if rotation and retention are configured well.

Summary: your fastest path from “site is down” to a real cause

Logs turn vague symptoms into a short, actionable list: failing URLs, the IPs driving the spike, and the error line that explains what broke. Start with 5xx counts and lock the time window. Then correlate with system events like OOM kills and disk pressure.

After you recover, fix rotation and keep a consistent baseline. The next incident will be easier to diagnose.

If you want a VPS environment that stays predictable under load—and gives you enough headroom to keep and analyze logs—use a HostMyCode VPS or move critical sites onto dedicated servers when you’ve outgrown shared resources.

VPS log analysis tutorial (2026): Find 500 errors, slow requests, and bot spikes using Nginx/Apache logs | HostMyCode