Nginx writes two kinds of logs: the access log (one line per request) and the error log (problems, warnings and debugging information). Together they answer most questions about what your web server is doing — who’s visiting, what’s slow, what’s failing and why.

Where the logs are

Default locations on Debian/Ubuntu and most distributions:

/var/log/nginx/access.log
/var/log/nginx/error.log

Per-site logs, if configured in server blocks:

access_log /var/log/nginx/example.com.access.log;
error_log  /var/log/nginx/example.com.error.log warn;

Not sure where a site logs? sudo nginx -T | grep -E 'access_log|error_log'. In Docker, the official image sends logs to stdout/stderr — read them with docker logs.

Reading the access log

The default combined format:

203.0.113.7 - - [04/Nov/2026:10:12:01 +0000] "GET /blog/ HTTP/2.0" 200 18234 "https://www.google.com/" "Mozilla/5.0 …"

Client IP, user (for basic auth), time, request line, status, bytes sent, referrer, user agent.

Add timing information

The default format doesn’t show how long requests took. Define a richer format in http {}:

log_format main_timed '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                      'rt=$request_time uct=$upstream_connect_time urt=$upstream_response_time '
                      'cache=$upstream_cache_status host=$host';

access_log /var/log/nginx/access.log main_timed;
  • $request_time — total time Nginx spent on the request.
  • $upstream_response_time — time the backend (PHP-FPM, app) took. If this is high, the application is slow; if request_time is high but upstream is low, the client or network is slow.
  • $upstream_cache_status — HIT/MISS for cached setups.

JSON logs for log tools

log_format json escape=json '{'
  '"time":"$time_iso8601","ip":"$remote_addr","method":"$request_method",'
  '"uri":"$request_uri","status":$status,"bytes":$body_bytes_sent,'
  '"rt":$request_time,"urt":"$upstream_response_time",'
  '"ref":"$http_referer","ua":"$http_user_agent","host":"$host"}';

JSON is easy to ship into Loki, Elasticsearch/OpenSearch or other log systems — see log management.

Error log levels

error_log /var/log/nginx/error.log warn;

Levels: debug, info, notice, warn, error (default), crit, alert, emerg. Common messages and what they mean:

Message Meaning
connect() failed (111: Connection refused) while connecting to upstream Backend down → 502 guide
upstream timed out (110) Backend too slow → 504 guide
open() "…" failed (13: Permission denied) File permissions → 403 guide
client intended to send too large body Upload limit → 413 guide
limiting requests Rate limiting triggered

Reduce noise

location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt  { access_log off; log_not_found off; }
location ~* \.(css|js|png|jpg|webp|svg|woff2)$ { access_log off; }

# Skip health checks
map $request_uri $loggable { /health 0; default 1; }
access_log /var/log/nginx/access.log main_timed if=$loggable;

Buffer writes on busy servers: access_log … buffer=64k flush=5s;.

Quick analysis from the shell

LOG=/var/log/nginx/access.log

tail -f $LOG                                              # live
awk '{print $9}' $LOG | sort | uniq -c | sort -rn         # status codes
awk '{print $1}' $LOG | sort | uniq -c | sort -rn | head  # top IPs
awk '$9 == 404 {print $7}' $LOG | sort | uniq -c | sort -rn | head   # top 404s
awk '$9 ~ /^5/' $LOG | tail                               # recent 5xx errors
grep -oE 'rt=[0-9.]+' $LOG | cut -d= -f2 | sort -n | tail # slowest request times
zcat $LOG.*.gz | awk '{print $7}' | sort | uniq -c | sort -rn | head  # older rotated logs

More one-liners in awk examples. For an interactive dashboard in the terminal, GoAccess (sudo apt install goaccess; goaccess $LOG --log-format=COMBINED) is excellent.

Rotation

Debian/Ubuntu’s package rotates logs daily via logrotate (/etc/logrotate.d/nginx), compressing old files and signalling Nginx to reopen its logs. Adjust retention there (rotate 14). See logrotate.

Real client IPs behind a proxy or CDN

Behind Cloudflare or a load balancer, $remote_addr is the proxy’s IP. Configure the real IP module so logs (and rate limits) see actual visitors — see real IP behind Cloudflare.

Privacy

Access logs contain IP addresses and user agents — personal data in many jurisdictions. Keep retention reasonable, restrict access to log files, and consider anonymising IPs if you don’t need them.

  • Nginx
  • Logs
  • Troubleshooting