Operate and troubleshoot
Read access and error logs
Correlate request records and error messages by time, host, URI, status, upstream, and request identifier.
8 minute lesson
Nginx writes two different stories. The access log records completed requests, one line each: who asked for what and what they got. The error log records operational details at configured severity levels: failed upstream connections, permission problems, configuration warnings. Debugging usually means reading both and connecting them.
On Ubuntu they live at /var/log/nginx/access.log and /var/log/nginx/error.log.
The default access format tells you the status code but not why a request was slow. Define a format that includes upstream timing and request identifiers:
log_format timed '$remote_addr [$time_local] "$request" $status '
'rt=$request_time urt=$upstream_response_time rid=$request_id';
access_log /var/log/nginx/access.log timed;
$request_time is the full time Nginx spent on the request, client included. $upstream_response_time is how long the application took. Comparing the two answers the eternal question “is it us or the app?” — a request with rt=9.1 urt=0.2 was slow because of the client or the network, not the upstream. $request_id is a unique value per request; forward it upstream as a header and you can match one Nginx line to one application log entry exactly.
Be deliberate about what you don’t log. Avoid logging secrets in query strings or headers — tokens in URLs, Authorization values, session cookies. Logs get shipped, backed up, and read by more people than the live database.
Correlate the two logs
Make one successful and one failing request against a test server whose upstream is stopped, then read both files:
tail -n 2 /var/log/nginx/access.log
# 203.0.113.7 [03/Aug/2026:17:02:11 +0000] "GET /health HTTP/1.1" 200 rt=0.000 urt=- rid=7f1c...
# 203.0.113.7 [03/Aug/2026:17:02:15 +0000] "GET /api/orders HTTP/1.1" 502 rt=0.001 urt=0.001 rid=9a44...
grep "connect() failed" /var/log/nginx/error.log | tail -n 1
# 2026/08/03 17:02:15 [error] 1205#1205: *18 connect() failed (111: Connection refused)
# while connecting to upstream, ... upstream: "http://127.0.0.1:3000/api/orders"
The access line says a 502 happened. The error line, at the same timestamp, says why: connection refused on port 3000. That pairing — status from the access log, cause from the error log — is the core troubleshooting move.
During an incident, narrow before you read: a tight time window plus a request ID beats scrolling. One config note: the error log’s level is set by error_log /var/log/nginx/error.log warn; — leave it at warn or error normally, and remember that a quiet error log at level error may hide warnings that explain tomorrow’s outage.
Lesson completed