Logging, metrics and debugging a config

Custom log formats, structured JSON access logs, error log levels, request IDs, stub_status metrics, and isolating a problem by bisecting the config.

Logs worth searching

log_format json_combined escape=json
  '{'
    '"time":"$time_iso8601",'
    '"remote_addr":"$remote_addr",'
    '"request":"$request",'
    '"status":$status,'
    '"bytes":$body_bytes_sent,'
    '"duration":$request_time,'
    '"upstream_time":"$upstream_response_time",'
    '"request_id":"$request_id",'
    '"user_agent":"$http_user_agent",'
  '}';

access_log /var/log/nginx/access.log json_combined buffer=32k flush=5s;

# propagate a request id from an upstream or generate one, and echo it back
add_header X-Request-Id $request_id always;

# per-location override for noisy or sensitive paths
location /health { access_log off; }
  • Log $request_time and $upstream_response_time separately: the difference is the time nginx itself spent.
  • A value of - in the upstream time means the request never reached an upstream, which usually points at a location or auth problem.
  • escape=json is essential — a user agent containing a quote otherwise breaks every log parser downstream.
  • buffer=32k flush=5s reduces disk writes dramatically under load with a bounded delay before lines appear.

Error levels and request ids

LevelContentUse in production
debugEverything, very verboseOnly while debugging a build with --with-debug
infoNormal operationsToo noisy for a busy server
noticeNormal but significant eventsReasonable default
warnRecoverable problemsYes, watch this level
errorFailed requestsYes, alert on it
# tail the error log while reproducing a problem
sudo tail -f /var/log/nginx/error.log

# correlate a single request across nginx and the app
curl -sS -H 'X-Request-Id: trace-1234' https://example.com/api/orders -o /dev/null
grep trace-1234 /var/log/nginx/access.log /var/log/app/app.log

# raise the level for one server only, then reload and revert
# server { error_log /var/log/nginx/debug.log info; }

Metrics and bisecting a config

location = /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

# the output tells you whether you have a capacity or a latency problem
# Active connections: 291
# server accepts handled requests
#  1203948 1203948 4093211
# Reading: 4 Writing: 18 Waiting: 269
# bisect: comment out includes until the problem disappears, then narrow
grep -n "include" /etc/nginx/nginx.conf
nginx -t && systemctl reload nginx

# confirm the parsed value of a directive rather than guessing
nginx -T | grep -A5 "server_name example.com"
💡
A large Waiting count with low Reading and Writing means idle keepalive connections holding sockets, not load. accepts equal to handled with a growing requests count is a healthy server; a gap between the first two means connections were dropped.

FAQ

Should I log everything as JSON?
Yes for anything a machine parses, and keep the plain format for a quick human read. The cost is a slightly larger log file; the benefit is not writing a regex for every question.
How do I debug a config change that broke one route?
Compare nginx -T before and after, then reproduce with curl against the specific URI. The expanded dump shows inherited values that the source files make easy to misread.

Installing nginx and understanding the configuration layout Rate limiting, access control and security headers

Last refreshed 2026-09-18.