How to read an nginx access log, line by line
Your server's access log records every request your site receives: every person, crawler, bot and attack. It looks like noise until you know what each part means. Here's a line taken apart field by field, the status codes worth knowing, and the patterns that tell you who's really visiting.
Analytics tools show you the visitors who run their script. The access log shows you everything that reached your server, whether it runs scripts or not. On our five sites, that difference was enormous: Analytics counted 256 daily visitors where the logs counted 69,881. Learning to read the log is how you see the rest.
Where to find it
# nginx
/var/log/nginx/access.log
# Apache on Debian or Ubuntu
/var/log/apache2/access.log
# Apache on Red Hat, CentOS or AlmaLinux
/var/log/httpd/access_log
Older days are usually kept next to it as access.log.1, access.log.2.gz and so on; most servers keep about two weeks. On shared hosting, look for Raw Access in cPanel or Logs in Plesk. To watch requests arrive live, run tail -f /var/log/nginx/access.log.
One line, field by field
nginx and Apache write the same "combined" format by default. Here's a typical line:
66.249.66.1 - - [16/Oct/2026:09:10:00 +0000] "GET /pricing/ HTTP/1.1" 200 5120 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
| Part | Field | What it tells you |
|---|---|---|
66.249.66.1 | Address | Where the request came from. The most useful field for telling bots from people, and the one you check to catch fake crawlers. |
- - | Identity and user | Almost always empty. Filled only for pages behind HTTP password protection. |
[16/Oct/2026:09:10:00 +0000] | Time | When the server received it, with the time zone offset (+0000 is UTC). |
"GET /pricing/ HTTP/1.1" | Request | The method (GET reads, POST sends data, HEAD asks for headers only), the address asked for, and the protocol. |
200 | Status | What the server answered. See the table below. |
5120 | Size | Bytes sent back, not counting headers. Useful for seeing what bots cost you in bandwidth. |
"-" | Referrer | The page that linked here, if the browser said. "-" means none: typed, bookmarked, or a bot. |
"Mozilla/5.0 (compatible; Googlebot/2.1; …)" | User agent | What the client says it is. It's a claim, not proof: anyone can send any user agent. |
This one claims to be Googlebot, and 66.249.66.1 is in Google's published crawler range, so it's the real thing. The same user agent from a cloud server would be a fake. You can check any address with our Googlebot checker.
Status codes you'll actually see
This is how the 400,425 page and API requests to our five sites were answered over 15 days:
| Code | Means | Share |
|---|---|---|
| 200 | OK: the server sent what was asked for | 65.6% |
| 404 | Not found | 22.4% |
| 410 | Gone: removed on purpose (we use it to turn away WordPress attacks quickly) | 4.7% |
| 301 / 302 | Redirect, permanent or temporary | 3.1% |
| 499 | nginx only: the client hung up before the answer was ready | 2.0% |
| 503 | Unavailable: overloaded, in maintenance, or rate-limited | 1.0% |
| 403 | Forbidden: refused, often by a firewall rule | 0.6% |
| 500 | Server error: something broke | 0.1% |
Our five sites, 2–16 Oct 2026, images, styles and scripts left out. Other codes made up the remaining 0.5%.
Two things surprise most people. First, nearly a quarter of all requests were for things that don't exist, and nearly 9 in 10 of those came from attacks, scripts and bots guessing addresses, not from broken links. Second, a 200 doesn't always mean what it seems: some sites answer every unknown address with their home page or an empty page, so an attack can get a 200 without finding anything. Check the size field: the same small size for many different addresses usually means a catch-all answer.
Five patterns worth spotting
- Attack paths. Requests for
/wp-login.php,/xmlrpc.php,/.envor/.git/configon a site that doesn't use them are scanners. On our sites, WordPress paths alone were asked for 286,061 times, and .env files 11,769 times. - POST floods. People mostly send GET requests. 60% of all requests to our sites were POSTs, and more than two-thirds of those went to
xmlrpc.php: password guessing, not visitors filling in forms. - Crawlers by name. Search the user agent for
Googlebot,bingbot,GPTBot,ClaudeBotorPerplexityBotto see who indexes your site, and then check that the addresses are really theirs. - Monitors. A steady drip of HEAD requests at regular intervals is usually an uptime monitor. Ours made 4,068 checks in 15 days.
- One address, many pages, no referrer, very fast. A person reads; a scraper fetches. Dozens of pages a minute from one address, with no images loaded and no referrer, is almost never a human.
Handy commands
# Top 10 addresses by number of requests
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# Top 10 pages asked for
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head
# Count of each status code
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# Every request claiming to be Googlebot
grep -i googlebot access.log
# All 404s, most common first
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head
These work on the default format. If your server writes a custom format, the field numbers change.
If your site is behind a CDN
If you use Cloudflare or another CDN, your log may show the CDN's addresses instead of your visitors'. Every request seems to come from a handful of addresses, and you lose the most useful field. Fix it by logging the real address: in nginx, the real_ip module with Cloudflare's address ranges and the CF-Connecting-IP header; in Apache, mod_remoteip. It's the single best improvement you can make to your logs.
Or let something read it for you
Reading a few hundred lines by hand teaches you a lot. Reading a few hundred thousand doesn't scale. Paste up to 5,000 lines into our free log analyzer and it sorts them the way this post describes: people, verified crawlers, fakes, AI and attacks. Humansift does the same for every request, every day, next to your Google Analytics.
How we measured
Every number here comes from the server logs of our own five websites (a pet guide, an education site, a small-business tool, a crafts marketplace and a fashion blog), from 2 to 16 October 2026: 400,425 page and API requests. Images, styles and scripts were counted separately and are left out. Each request was sorted by Humansift: named bots by their user agent, Googlebot, Bingbot, GPTBot, ChatGPT and Perplexity checked against the address ranges their owners publish, attack paths by pattern, and data-centre traffic by network. A "visitor" is one address and browser on one day. Five small sites are a small sample, so read these as one honest data point, not an industry average.