Guide
Nginx Access Log Format Explained
Every request nginx serves produces one line in the access log. Once you know how that line is built, the whole file stops looking like noise.
The default nginx access log uses the combined format. It packs the client address, the timestamp, the request itself, the response status, how many bytes went back, and two HTTP headers into a single space-separated line. Here is a real line so you can see what you are working with:
203.0.113.42 - - [12/Sep/2026:13:55:36 +0000] "GET /pricing HTTP/1.1" 200 2326 "https://example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36"The combined format, field by field
The combined format is defined inside nginx itself. The directive looks like this, and it lives in thehttp block of your config:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';Match each variable against the example line above and the structure falls into place. Here is what each piece means.
| Field | Variable | What it holds |
|---|---|---|
| Client IP | $remote_addr | The address that opened the connection. Behind a proxy or load balancer this is often the proxy, not the visitor. |
| Ident | hard-coded - | The old RFC 1413 identity field. Nobody runs identd anymore, so it is always a dash. |
| Remote user | $remote_user | The username from HTTP basic auth. A dash when the request was not authenticated. |
| Timestamp | $time_local | Local server time in the day/month/year:hour:minute:second zone shape, wrapped in square brackets. |
| Request line | $request | The raw request line: method, path, and protocol, like GET /pricing HTTP/1.1. Quoted because it contains spaces. |
| Status | $status | The HTTP response code nginx sent back. See the status code reference. |
| Bytes sent | $body_bytes_sent | Size of the response body in bytes, not counting headers. Useful for bandwidth math. |
| Referer | $http_referer | The page the visitor came from, if the browser sent it. Often a dash. |
| User agent | $http_user_agent | The client software string. This is where browsers, crawlers, and bots announce themselves. |
Common format (CLF)
Before combined became the default, the Common Log Format was the standard. You still see it on older setups and on servers that were configured to match Apache. It is the same line as combined, just without the referer and user agent on the end:
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sentIf your logs stop after the byte count, you are looking at CLF. You lose referer and user agent, which makes bot detection much harder, so most people switch to combined the first time they need to track where traffic comes from.
A note on the request line
The $request field trips up a lot of parsers because it has to be split again into three parts. A malformed request will show up here too. If you see something like "\x16\x03\x01" where a normal request line should be, that is usually someone sending raw TLS bytes at your plain HTTP port, or a scanner probing for something. Those lines often pair with a 400 status.
Customizing the format
You can define your own format and point an access_log directive at it. A common addition is request timing and the real client IP when nginx sits behind a CDN or load balancer:
log_format extended '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'xff="$http_x_forwarded_for"';
access_log /var/log/nginx/access.log extended;A few variables worth knowing when you build a custom format:
$request_timeis the full time nginx spent on the request, in seconds with millisecond resolution. This is how you find slow endpoints.$http_x_forwarded_forcarries the original client IP when a proxy forwards it. When$remote_addris always your load balancer, this is the field you actually want.$upstream_response_timetells you how long your backend took, separate from nginx itself. Handy for deciding whether a slow page is the app or the proxy.$request_lengthand$bytes_sentgive you fuller traffic accounting than$body_bytes_sentalone.
One thing to watch: if you change the format, anything that parses the log needs to know. A field added in the middle shifts everything after it. Adding new fields to the end is the safer move.
Where to go next
Now that the line structure is clear, the natural next steps are reading the status codes and pulling signal out of the file. Start with the step-by-step analysis guide, or learn to spot unwanted crawlers in how to detect bad bots.
Try the free nginx log analyzer
Paste or drop your access log and get instant parsing, traffic breakdowns, and bot detection. Everything runs in your browser, so the file never leaves your machine.
Open the analyzer