Reading AI Crawlers in Your Server Logs: A Practical Field Guide
Server access logs are the only verifiable record of AI crawler activity. Here's how to read GPTBot, ClaudeBot, and PerplexityBot traffic with awk, grep, and reverse DNS — then turn it into a weekly citation dashboard.
Your server access logs are the only verifiable record of how often GPTBot, ClaudeBot, and PerplexityBot actually visit your site, which URLs they re-fetch, and whether they hit you for training or in real time during a user's prompt. Third-party analytics filter out bot traffic, and Search Console does not see AI crawlers at all — but every request lands in your access.log. With a handful of awk and grep one-liners plus reverse DNS verification, you can turn raw log lines into a per-bot citation dashboard that tells you what each engine values about your content.
Why server logs are the ground truth
Most analytics stacks — GA4, Plausible, Mixpanel — explicitly filter out non-human traffic. That filter is useful for product metrics and useless for AI visibility. If you want to know whether ChatGPT just fetched your Stripe-style pricing page to answer a user's question, the only place that fact exists is your web server's access log.
Logs also separate two very different crawl modes that look identical from the outside:
- Training crawlers sweep large batches of URLs to refresh an engine's index. They visit on schedules the engine controls.
- On-demand fetchers make real-time requests when a user prompts the assistant. "Summarize this Notion docs page" triggers a live HTTP GET right then.
The on-demand class is what matters for citations. A surge of Claude-User or Perplexity-User hits on a specific URL is the closest signal you will get that the content is being read into responses today, not next quarter.
Bot identity: real User-Agent strings
To analyze AI bot traffic, you need to know what each one actually sends. The major three:
- GPTBot — OpenAI's training crawler. The User-Agent contains
GPTBotand OpenAI publishes both the UA string and the JSON file of IP ranges from which it originates (OpenAI — Overview of OpenAI Crawlers). OpenAI also runsChatGPT-Userfor on-demand fetches andOAI-SearchBotfor the ChatGPT search backend. - ClaudeBot — Anthropic's general training crawler. Claude-User (sometimes appearing as
claude-web) handles per-prompt fetches when a Claude user shares a URL or asks Claude to read one. Anthropic documents both, including how to allow or block each independently (Anthropic Support — Does Anthropic crawl data from the web?). - PerplexityBot for indexing and Perplexity-User for live user-triggered fetches — with different robots.txt obedience profiles (Perplexity Docs — PerplexityBot).
All three reference the Robots Exclusion Protocol as standardized in RFC 9309. User-Agent alone is not proof of identity — anyone can set a header. Verification requires a forward-confirmed reverse DNS lookup against the source IP, or a match against the published IP-range JSON when the vendor offers one.
# Verify a GPTBot hit by reverse DNS
host 20.171.207.113
# → 113.207.171.20.in-addr.arpa domain name pointer <hostname>.openai.com.
host -t A <hostname returned above>
# Confirm forward lookup matches the original IP
If the round-trip does not resolve back to an OpenAI-owned hostname, treat the User-Agent as untrusted.
Awk and grep recipes for Nginx and Apache
The default Nginx combined log format puts the User-Agent in field 12, the URL path in field 7, and the status code in field 9. Apache's combined format has the same shape, so these one-liners work on either with no changes.
Count requests per AI bot per day:
awk '/GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|PerplexityBot|Perplexity-User/ {
split($4, d, ":"); date = substr(d[1], 2)
for (b in bots) if ($0 ~ b) hits[date " " b]++
}
BEGIN { split("GPTBot ChatGPT-User OAI-SearchBot ClaudeBot Claude-User PerplexityBot Perplexity-User", bots, " ") }
END { for (k in hits) print k, hits[k] }' access.log | sort
Top 20 URLs each bot re-fetches:
grep "GPTBot" access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
Repeat with ClaudeBot, Claude-User, and so on. The contrast is what's interesting — the URL list under GPTBot is your training-crawl footprint; the list under Claude-User is what real Claude users are asking about today.
Status-code breakdown per bot (to catch 404 / 5xx that hurt citation eligibility):
grep "PerplexityBot" access.log | awk '{print $9}' | sort | uniq -c
A spike in 404s from PerplexityBot after a URL restructure means your sitemap and the engine's index are out of sync — citations for those URLs disappear until re-indexed.
Crawl-gap analysis (hours between successive hits to the same URL):
grep "GPTBot" access.log | awk '{print $4, $7}' | sort -k2 | \
awk '{ if (prev==$2) print $2, "gap:", $1, "after", prev_t; prev=$2; prev_t=$1 }'
This surfaces URLs the engine considers worth re-checking frequently — your high-value pages from the engine's perspective.
What crawl frequency and depth actually signal
Re-crawl cadence is a useful proxy for perceived value. A page GPTBot fetches weekly is one OpenAI's pipeline considers fresh-sensitive. A page it fetched once a year ago and never returned to is indexed but stale.
The strongest citation signal is on-demand fetcher activity:
Claude-Userhits within minutes of a content update suggest a real user just prompted Claude with your URL.Perplexity-Userhits in clusters across multiple URLs in your domain mean Perplexity is composing an answer that draws on your site as a source.OAI-SearchBothits at low cadence to specific URLs are ChatGPT's search backend deciding which pages to surface in answers.
A sudden drop in either training or on-demand hits after a content change deserves investigation. Most often it's a 5xx burst the engine encountered, a noindex accidentally added, or a robots.txt token you did not realize applied.
Turning logs into a weekly per-bot dashboard
A minimal SQLite schema is enough to track this over time:
CREATE TABLE bot_hits (
date TEXT,
bot TEXT,
url TEXT,
status INTEGER,
hits INTEGER,
PRIMARY KEY (date, bot, url, status)
);
A nightly cron parses yesterday's log into this table. Once you have two to three weeks of data, week-over-week comparison per bot reveals patterns the day-level view buries. A typical row in a weekly diff might read: Bot: Claude-User · URL: /pricing · Last week: 14 hits · This week: 47 hits · Δ: +33.
Useful alerting rules:
- 5xx surge from one bot — bad CDN config, or the bot saw something it now avoids.
- Zero-fetch week from a bot that previously crawled daily — possible deprioritization.
- New URL never crawled within 14 days — sitemap or internal linking gap.
Day-level numbers are noisy because vendors batch crawls unpredictably. Weekly aggregates filter the noise. Cloudflare's network-wide measurements show GPTBot is the highest-volume AI training crawler across sites, with substantial traffic from ClaudeBot and PerplexityBot and a meaningful share of operators blocking at least one AI bot (Cloudflare — Declaring your AIndependence). Your own logs will look proportionally similar.
Common pitfalls
Spoofed UAs. Scraping farms set their UA to GPTBot to bypass blocks meant for them but allow real OpenAI traffic. Always verify by IP range or reverse DNS before trusting a UA string.
Confusing training and on-demand. Blocking ClaudeBot in robots.txt does not block Claude-User — those are separate tokens with separate policies. If you want to be in Claude's answers but not in its training set, allow Claude-User and disallow ClaudeBot.
Aggressive log rotation. Many default Nginx setups rotate daily and keep seven days. That makes week-over-week analysis impossible. Keep at least 30 days of compressed logs.
Reading every AI hit as a citation event. A GPTBot fetch is index input, not a live answer. A Perplexity-User fetch on the same URL ten minutes later is the citation. The dashboard is more useful when those two columns sit side by side.
Logs do not lie. Once you have the recipes wired up, you will know within a week which URLs each engine treats as worth surfacing — and which ones you have been writing for nobody.
Deniz
Content & GEO Strategy