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_timeand$upstream_response_timeseparately: 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=jsonis essential — a user agent containing a quote otherwise breaks every log parser downstream.buffer=32k flush=5sreduces disk writes dramatically under load with a bounded delay before lines appear.
Error levels and request ids
| Level | Content | Use in production |
|---|---|---|
debug | Everything, very verbose | Only while debugging a build with --with-debug |
info | Normal operations | Too noisy for a busy server |
notice | Normal but significant events | Reasonable default |
warn | Recoverable problems | Yes, watch this level |
error | Failed requests | Yes, 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.Related
Installing nginx and understanding the configuration layout Rate limiting, access control and security headers
Last refreshed 2026-09-18.