Guides
Guide

nginx Logs: Where They Live and How to Configure Them

By Sazabi Team

Last verified on Nov 8, 2026.

nginx writes two logs. The access log records one line per request, and the error log records problems nginx itself hits, such as a refused upstream connection or a missing file. Both are configured with a few directives: access_log and log_format for requests, and error_log for errors. This guide covers the defaults, a JSON access format, conditional logging, and how to keep the files from growing without bound.

Where nginx Logs Live

The built-in default is logs/access.log with the combined format, resolved against the nginx prefix, and logs/error.log at level error. Distribution packages override this. On Debian and Ubuntu the shipped nginx.conf points to /var/log/nginx/access.log and /var/log/nginx/error.log.

sudo nginx -T 2>/dev/null | grep -E '^\s*(access_log|error_log)'
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

nginx -T prints the full effective configuration, so it shows the paths that are really in use, including those in included files.

The Combined Access Log Format

If you do not name a format, nginx uses the predefined combined format:

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

A line in that format looks like this:

203.0.113.7 - - [08/Nov/2026:09:14:02 +0000] "GET /api/orders HTTP/1.1" 502 157 "-" "curl/8.5.0"

The format has no request time and no upstream details. For an app behind a proxy, those are the fields you reach for first, so most teams define their own.

Write the Access Log as JSON

Structured logging makes access logs searchable without regular expressions. Set escape=json on log_format. With it, nginx escapes " and \ and control characters in variable values so one request is always one valid JSON object. The default escaping is a different, \xXX style that JSON parsers reject.

http {
    log_format json_combined escape=json
      '{'
        '"time":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"request_id":"$request_id",'
        '"method":"$request_method",'
        '"uri":"$request_uri",'
        '"status":$status,'
        '"bytes_sent":$body_bytes_sent,'
        '"request_time":$request_time,'
        '"upstream_addr":"$upstream_addr",'
        '"upstream_status":"$upstream_status",'
        '"upstream_response_time":"$upstream_response_time",'
        '"user_agent":"$http_user_agent"'
      '}';

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

Running this config against nginx 1.28 and sending a request with a user agent of a"b produced:

{
  "time": "2026-11-08T06:44:16+00:00",
  "remote_addr": "127.0.0.1",
  "request_id": "f6b3ab377185641fa12b99c4a5433540",
  "method": "GET",
  "uri": "/",
  "status": 200,
  "bytes_sent": 2,
  "request_time": 0.0,
  "upstream_addr": "",
  "upstream_status": "",
  "upstream_response_time": "",
  "user_agent": "a\"b"
}

Three details matter here:

  • Quote the upstream variables. When nginx tries more than one upstream server, $upstream_addr, $upstream_status, and $upstream_response_time hold comma-separated lists, which is not a valid JSON number.
  • $request_time is a number and $status is an integer, so they are left unquoted. That lets a query compare them numerically.
  • $request_id is a unique identifier generated from 16 random bytes in hexadecimal. Pass it upstream with proxy_set_header X-Request-ID $request_id; and log it in the application, and one request can be followed across both logs. See correlation IDs.

Read the result with jq:

jq -r 'select(.status >= 500) | [.time, .status, .uri, .upstream_response_time] | @tsv' /var/log/nginx/access.json.log

For a longer treatment of the JSON side, see the JSON logging guide.

Buffer, Compress, and Filter Writes

access_log accepts parameters after the format name. buffer= makes writes buffered, flush= sets the longest time a line can wait in the buffer, gzip compresses the buffered data, and if= makes logging conditional. Buffered data is also written when the next line does not fit, and when a worker re-opens its log files or shuts down. The buffer must not exceed the size of an atomic disk write.

Conditional logging uses map. nginx skips the request if the condition evaluates to 0 or an empty string. The documented example drops 2xx and 3xx responses:

map $status $loggable {
    ~^[23]  0;
    default 1;
}

access_log /var/log/nginx/access.log combined if=$loggable;

A map on the request URI keeps health checks out of the file:

map $request_uri $log_it {
    /healthz  0;
    default   1;
}

access_log /var/log/nginx/access.json.log json_combined if=$log_it;

To drop a whole location, use access_log off; inside it. The off value cancels every access_log directive on that level. Dropping lines on purpose is a sampling decision, so read log sampling before you filter out successes, and never filter the lines you need for audit or security work.

Error Log Levels

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

The second parameter is the level: debug, info, notice, warn, error, crit, alert, or emerg, in increasing severity. A level logs itself and everything more severe, so the default error records error, crit, alert, and emerg. Omit the level and you get error.

A refused upstream connection at error level looks like this, captured from the same test server:

2026/11/08 06:44:16 [error] 19230#19230: *2 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: , request: "GET /api/x HTTP/1.1", upstream: "http://127.0.0.1:9/api/x", host: "localhost:8088"

The fields to read are the bracketed level, the worker PID and connection number (19230#19230: *2), the reason in parentheses, and the client, request, and upstream values.

debug logging needs a binary built with --with-debug, which you can check with nginx -V. The packaged nginx-debug binary has it. Redefining error_log at a lower level, for example in a server block, turns debug logging off for that block, so repeat the debug level there.

nginx 1.29.8 added a json parameter to error_log. The docs list it as part of the commercial subscription, so do not rely on it with open source nginx. Open source builds write the plain text line above.

Send Logs to Syslog

Both directives can write to syslog by using a syslog: prefix:

error_log  syslog:server=192.0.2.10:514,tag=nginx_err warn;
access_log syslog:server=unix:/var/log/nginx.sock,facility=local7,tag=nginx,severity=info json_combined;

server= takes an address or unix: socket path, and the default is UDP port 514. facility= defaults to local7, tag= to nginx, and severity= to info for access logs. The severity of error messages is chosen by nginx, so severity= is ignored on error_log. See syslog for what the fields mean.

Rotate nginx Logs

nginx keeps its log files open, so moving a file does not move the stream. Rename or compress the file, then send USR1 to the master process so nginx re-opens the file at the original path:

sudo mv /var/log/nginx/access.log /var/log/nginx/access.log.1
sudo kill -USR1 "$(cat /var/run/nginx.pid)"

You will usually let logrotate do this with a postrotate script that sends the same signal. The PID file path comes from the pid directive.

Sending nginx Logs to Sazabi

Sazabi, an AI-native observability and security platform, does not have an nginx-specific source, but its OpenTelemetry Collector source takes any receiver. Point the Collector's filelog receiver at /var/log/nginx/access.json.log, add the otlphttp/sazabi exporter with your intake URL, and put it in the logs pipeline. The Fluent Bit output works the same way with a tail input. See the OpenTelemetry Collector integration and Fluent Bit pages for setup, then use the client errors and attack payloads guides for what to look for in the access log.

Frequently Asked Questions

Where are nginx logs stored?

The built-in defaults are logs/access.log and logs/error.log under the nginx prefix. Debian and Ubuntu packages set /var/log/nginx/access.log and /var/log/nginx/error.log. Run nginx -T and search the output for access_log and error_log to see what your server uses.

How do I change the nginx log format?

Define a format with log_format in the http block, then reference its name in an access_log directive. For JSON, add escape=json to log_format so quotes and control characters in variables are escaped.

What is the default nginx error log level?

The default level is error, which logs error, crit, alert, and emerg messages. Lower levels such as warn, notice, info, and debug add more detail, and debug needs an nginx binary built with --with-debug.

How do I stop logging health checks in nginx?

Set access_log off in the health check location, or use a map on $request_uri together with the if= parameter of access_log so matching requests are skipped.

Sources

  1. access_log syntax and default, the predefined combined format, log_format escape=json, buffer and flush parameters, and conditional logging with if= and map (accessed Nov 8, 2026)
  2. error_log syntax, the eight levels from debug to emerg, the default level error, and that the json parameter is a commercial subscription feature (accessed Nov 8, 2026)
  3. error_log and access_log can write to syslog with server=, facility=, severity=, and tag= parameters (accessed Nov 8, 2026)
  4. The USR1 signal re-opens log files (accessed Nov 8, 2026)
  5. The $upstream_addr, $upstream_status, and $upstream_response_time variables and their comma-separated values for multiple upstream attempts (accessed Nov 8, 2026)
  6. $request_id is a unique request identifier generated from 16 random bytes, in hexadecimal (accessed Nov 8, 2026)
  7. An OpenTelemetry Collector with a filelog receiver and an otlphttp exporter forwards logs to Sazabi (accessed Nov 8, 2026)