The Enterprise CDN Log Analysis Cookbook for Technical SEO
When diagnosing search visibility on large e-commerce or marketplace platforms, Google Search Console only gives you a delayed, sampled view of what happened. And if you only look at origin server logs, you miss a massive piece of the puzzle: cached responses served at the network edge.
Modern websites run behind Content Delivery Networks (Cloudflare, Akamai, Fastly, AWS CloudFront). The CDN is where bot management happens, where redirects fire, where edge caching offloads origin servers, and where WAF rules can accidentally block legitimate crawlers. This guide is a vendor-agnostic CDN Log Analysis Cookbook—featuring SQL, Grafana LogQL, and terminal recipes you can use to monitor search and AI bots, audit crawl efficiency, and troubleshoot edge errors.
1. Why Analyze Logs at the CDN Layer Instead of Origin?
A CDN sits between the crawler and your origin infrastructure. Analyzing logs exclusively at the origin creates three major blind spots:
- Invisible Cached Bot Traffic: When Akamai, Fastly, or Cloudflare serves a cached HTML page or static asset (HIT), that request never touches your origin server. Origin logs drastically undercount actual crawler activity.
- Edge-Level Blocking (WAF & Rate Limiters): If a bot-management policy or rate limiter serves a 403 Forbidden, 429 Too Many Requests, or a CAPTCHA challenge at the edge, your origin server never records the failed request.
- Edge Redirects & Header Transforms: Redirects (301/302), X-Robots-Tag headers, and canonical rules executed via Edge Workers happen before origin routing.
2. Standardizing Your CDN Log Schema
Whether you export logs from Akamai DataStream, Fastly Real-Time Log Streaming, or Cloudflare Logpush into BigQuery, Snowflake, or DuckDB, most CDN tables map to a shared set of core telemetry fields:
req_timestamp -> Timestamp or date partition of the request (e.g. 2026-10-15 14:22:01 UTC)
req_host -> Hostname / market domain (e.g. www.example.com)
req_path -> URL path without query string (e.g. /products/item-12345)
query_str -> URL query parameters (e.g. color=black&sort=price)
status_code -> HTTP response status sent to client (200, 301, 304, 403, 500)
user_agent -> Raw User-Agent string (e.g. Mozilla/5.0 ... Googlebot/2.1)
cache_status -> Edge cache outcome (HIT, MISS, REVALIDATED, BYPASS)
turnaround_time_ms -> Edge response time / TTFB / TAT in milliseconds
client_asn -> Network ASN for bot verification (e.g. 15169 for Google LLC)
trace_id -> Unique edge request ID for end-to-end debugging 3. The Four Main Crawler Ecosystems: Google, Bing, OpenAI & Claude
Before running SQL queries on your logs, you need a clear taxonomy of who is hitting your edge and why. Today’s crawler landscape is split across core search indexes, commercial ad/shopping verification, AI model training, and real-time user-triggered AI retrieval (RAG).
- Google (Googlebot, Google-InspectionTool, GoogleOther, AdsBot-Google, Storebot-Google): Never block Googlebot or Google-InspectionTool (ASN 15169)—blocking InspectionTool causes live URL tests in Search Console to fail with 403/5xx errors. GoogleOther handles non-ranking R&D fetches and can be safely rate-limited if it spikes origin load. AdsBot and Storebot verify paid landing pages and Merchant Center shopping feeds.
- Bing / Microsoft (bingbot, adidxbot, BingPreview): High priority for both traditional search and partner AI search ecosystems. Supports IndexNow for rapid crawl pickup and respects Cache-Control and 304 conditional requests (ASN 8075).
- OpenAI / ChatGPT (OAI-SearchBot, ChatGPT-User, GPTBot): OAI-SearchBot indexes content for citations and links inside ChatGPT Search. ChatGPT-User fetches pages in real time when a human user asks a question. GPTBot crawls content for foundation model training—meaning you can block GPTBot without losing visibility in ChatGPT Search.
- Anthropic / Claude (Claude-SearchBot, Claude-User, ClaudeBot): Claude-SearchBot indexes pages to ground and cite sources in Claude search results, while Claude-User performs live fetches when a user queries a URL. ClaudeBot handles bulk model pre-training and can be governed separately.
Governance Rule: Never rely on the User-Agent string alone. Third-party audit tools and scrapers routinely spoof Googlebot. Cross-check User-Agents against ASN/IP ranges and filter out your own internal monitoring scripts.
4. SQL Recipes for Bot & Crawl Analysis
Recipe 1: Multi-Bot Traffic Overview (Search + AI Crawlers)
This query segments the last 90 days of crawler traffic across Google, Bing, OpenAI, Claude, and Apple by page template, mobile vs. desktop UA, clean vs. parameterized URLs, and HTTP status class. Notice that it computes both total requests (bandwidth load) and distinct URLs crawled (true catalog coverage):
SELECT
DATE(req_timestamp) AS crawl_date,
req_host,
-- 1. Map URL paths to page template buckets
CASE
WHEN req_path LIKE 'products/%' OR req_path LIKE 'itm/%' THEN 'PDP (Product Detail)'
WHEN req_path LIKE 'c/%' OR req_path LIKE 'b/%' THEN 'PLP (Category Browse)'
WHEN req_path LIKE 's/%' OR req_path LIKE 'search/%' THEN 'Internal Search SRP'
WHEN req_path LIKE 'stores/%' THEN 'Merchant Storefront'
WHEN req_path LIKE 'blog/%' OR req_path LIKE 'guides/%' THEN 'Editorial / Guides'
WHEN req_path IS NULL OR req_path = '' OR req_path = '/' THEN 'Homepage'
ELSE 'Other'
END AS page_template,
-- 2. Device User-Agent type
CASE
WHEN LOWER(user_agent) LIKE '%mobile%' THEN 'Mobile UA'
ELSE 'Desktop UA'
END AS device_ua,
-- 3. Clean vs. Parameterized URL
CASE
WHEN query_str IS NULL OR LENGTH(query_str) = 0 THEN 'Clean / Canonical'
ELSE 'Parameterized'
END AS url_type,
-- 4. HTTP Status Class
CASE
WHEN CAST(status_code AS STRING) LIKE '2%' THEN '2xx Success'
WHEN CAST(status_code AS STRING) LIKE '3%' THEN '3xx Redirect'
WHEN CAST(status_code AS STRING) LIKE '4%' THEN '4xx Client Error'
WHEN CAST(status_code AS STRING) LIKE '5%' THEN '5xx Server Error'
ELSE 'Other'
END AS status_bucket,
status_code,
-- 5. Bot Family Classification
CASE
WHEN LOWER(user_agent) LIKE '%google%' THEN 'Google'
WHEN LOWER(user_agent) LIKE '%bing%' THEN 'Bing'
WHEN LOWER(user_agent) LIKE '%chatgpt-user%'
OR LOWER(user_agent) LIKE '%gptbot%'
OR LOWER(user_agent) LIKE '%oai-searchbot%' THEN 'OpenAI / ChatGPT'
WHEN LOWER(user_agent) LIKE '%claude-user%'
OR LOWER(user_agent) LIKE '%claude-searchbot%'
OR LOWER(user_agent) LIKE '%claudebot%' THEN 'Anthropic / Claude'
WHEN LOWER(user_agent) LIKE '%perplexity%' THEN 'Perplexity'
WHEN LOWER(user_agent) LIKE '%applebot%' THEN 'Apple'
ELSE 'Other Bot'
END AS bot_family,
-- 6. Total Requests vs. Unique URLs Crawled
COUNT(*) AS total_requests,
COUNT(DISTINCT CONCAT('https://', req_host, '/', req_path,
IF(LENGTH(query_str) > 0, CONCAT('?', query_str), ''))) AS unique_urls_crawled
FROM `project.dataset.cdn_bot_logs`
WHERE
req_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
AND (
LOWER(user_agent) LIKE '%googlebot%'
OR LOWER(user_agent) LIKE '%googleother%'
OR LOWER(user_agent) LIKE '%adsbot-google%'
OR LOWER(user_agent) LIKE '%bingbot%'
OR LOWER(user_agent) LIKE '%applebot%'
OR LOWER(user_agent) LIKE '%chatgpt-user%'
OR LOWER(user_agent) LIKE '%gptbot%'
OR LOWER(user_agent) LIKE '%oai-searchbot%'
OR LOWER(user_agent) LIKE '%claude-user%'
OR LOWER(user_agent) LIKE '%claude-searchbot%'
OR LOWER(user_agent) LIKE '%perplexitybot%'
)
AND LOWER(user_agent) NOT LIKE '%internal_monitoring%'
AND LOWER(user_agent) NOT LIKE '%screaming frog%'
GROUP BY 1, 2, 3, 4, 5, 6, 7, 8
ORDER BY crawl_date DESC, total_requests DESC; Recipe 2: HTTP Status Codes, Edge Cache HITs vs. 304 Not Modified
Caching and 304 Not Modified are complementary but distinct mechanisms. When a browser reuses its local cache during max-age, zero network requests occur. When a CDN serves an Edge Cache HIT, the edge returns 200 OK with the body without hitting your origin server. When Googlebot sends an If-Modified-Since or If-None-Match header and the server returns 304 Not Modified with no body, Google reuses its previously crawled version—saving bandwidth and origin compute.
Recipe 3: Quantifying Crawl Waste on Out-of-Stock / Expired Listings
On large marketplaces, millions of ended or out-of-stock PDPs continue returning 200 OK and getting recrawled uncached from origin every day. Join your CDN bot logs against your product catalog table to measure how much Googlebot activity is spent on unavailable inventory and verify whether those responses are cached at the edge:
SELECT
p.inventory_status,
c.cache_status,
c.status_code,
COUNT(c.trace_id) AS googlebot_requests,
COUNT(DISTINCT p.product_id) AS distinct_products_crawled
FROM `project.dataset.product_catalog` p
INNER JOIN `project.dataset.cdn_bot_logs` c
ON p.product_id = REGEXP_EXTRACT(c.req_path, r'products/(\d+)')
WHERE
c.req_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND LOWER(c.user_agent) LIKE '%googlebot%'
AND p.stock_quantity <= 0
AND c.status_code = 200
GROUP BY 1, 2, 3
ORDER BY googlebot_requests DESC; Recipe 4: Auditing Sitemap & Submission Pickup Velocity
Submitting URLs via XML sitemaps or IndexNow does not guarantee immediate crawling. Use Common Table Expressions (CTEs) to join your submitted URL log against CDN bot requests to measure pickup rates—or filter WHERE c.clean_url IS NULL to isolate submitted pages that crawlers ignored:
WITH cdn_crawls AS (
SELECT
DATE(req_timestamp) AS crawl_date,
CONCAT('https://', req_host, '/', req_path) AS clean_url,
COUNT(DISTINCT trace_id) AS bot_requests
FROM `project.dataset.cdn_bot_logs`
WHERE
req_timestamp BETWEEN '2026-10-01' AND '2026-10-15'
AND LOWER(user_agent) LIKE '%googlebot%'
GROUP BY 1, 2
),
submitted_urls AS (
SELECT
DATE(submitted_at) AS sub_date,
canonical_url,
page_template
FROM `project.dataset.url_submissions`
WHERE DATE(submitted_at) BETWEEN '2026-10-01' AND '2026-10-15'
)
SELECT
s.page_template,
COUNT(DISTINCT s.canonical_url) AS total_submitted_urls,
COUNT(DISTINCT c.clean_url) AS total_crawled_urls,
ROUND(SAFE_DIVIDE(COUNT(DISTINCT c.clean_url), COUNT(DISTINCT s.canonical_url)) * 100, 2) AS crawl_pickup_rate_pct
FROM submitted_urls s
LEFT JOIN cdn_crawls c
ON s.canonical_url = c.clean_url
AND c.crawl_date >= s.sub_date
GROUP BY 1
ORDER BY total_submitted_urls DESC; Recipe 5: Detecting Malformed URLs & Double-Encoding Loops
Broken percent-encoding (such as %2520 in category paths) frequently causes 301/302 redirect loops that trap crawlers:
SELECT
DATE(req_timestamp) AS req_date,
CONCAT('https://', req_host, '/', req_path) AS req_url,
COUNT(trace_id) AS redirect_requests
FROM `project.dataset.cdn_bot_logs`
WHERE
req_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 14 DAY)
AND LOWER(user_agent) LIKE '%googlebot%'
AND status_code BETWEEN 300 AND 399
AND req_path LIKE '%\%%' ESCAPE '\'
GROUP BY 1, 2
ORDER BY redirect_requests DESC
LIMIT 100; 5. Real-Time Edge Monitoring with Grafana & Loki (LogQL)
Warehouse SQL tables often have a 12-to-24-hour ingestion delay. When deploying new routing rules, migrating to SSR streaming, or debugging why Google Search Console's live 'Test Live URL' (Google-InspectionTool) is returning 5xx errors, Grafana + Loki (LogQL) gives you instant visibility:
# 1. Real-Time Googlebot Status Code Distribution
sum by (res_status) (
count_over_time(
{namespace="cdn_edge_logs", req_host="www.example.com", req_user_agent=~".*Googlebot.*"}
| logfmt [$__auto]
)
)
# 2. Isolate Edge Error Codes for GSC Live Inspection Tool 5xx Failures
sum by (edge_error_code) (
count_over_time(
{namespace="cdn_edge_logs", req_host="www.example.com", client_asn="15169", agent=~".*InspectionTool.*", res_status=~"5.."}
| logfmt [$__auto]
)
) 6. Multi-CDN & WAF Troubleshooting + Sitemap Unpacking
In dual-CDN architectures (e.g., Akamai + Fastly), always compare status codes and turnaround time (TAT) by provider. If 403 or 429 spikes occur on only one provider, inspect drifted WAF or rate-limiter rules across your security layers (CDN WAF -> Bot Manager -> Application Rate Limiter). And when you need to unpack compressed XML sitemaps locally into CSV chunks for warehouse joins, use these bash one-liners:
# Extract all <loc> URLs from compressed .xml.gz sitemaps with their source filename
for file in *.xml.gz; do
gunzip -c "$file" | grep '<loc' | grep -oE 'https://[^<]+' | while read -r url; do
echo "$url,$file" >> sitemap_urls.txt
done
done
# Find duplicate URLs across sitemap files
cut -d',' -f1 sitemap_urls.txt | sort | uniq -d > duplicate_sitemap_urls.txt
# Split into 200MB CSV chunks with headers for warehouse upload
split -b 200M sitemap_urls.txt part_
for file in part_*; do
echo "url,sitemap_file" > "${file}.csv"
cat "$file" >> "${file}.csv"
rm "$file"
done Why should technical SEOs analyze CDN logs instead of origin server logs?
Because modern CDNs (Akamai, Fastly, Cloudflare) serve cached HTML and static assets directly at the network edge, those requests never reach the origin server. Origin logs also miss requests blocked by edge WAFs, rate limiters (403/429), and edge redirects.
What is the difference between a 200 Edge Cache HIT and a 304 Not Modified response?
A 200 Edge Cache HIT serves the full response body from the CDN edge without contacting the origin server. A 304 Not Modified occurs when a client or crawler sends a conditional request (If-Modified-Since or If-None-Match) and the server confirms the content has not changed, returning headers only with no response body.
How do you block AI model training without losing visibility in ChatGPT or Claude search?
Govern training crawlers separately from search and user-triggered retrieval bots. You can disallow training tokens like GPTBot and ClaudeBot while allowing discovery and citation agents like OAI-SearchBot, ChatGPT-User, Claude-SearchBot, and Claude-User.
- Adding an agent to my personal blog repo: Is it worth it? →
- Hello, I'm Hermes: Caroline's new assistant engineer →
- Why My Blog Was Down For Weeks (And The Netlify DNS Mistake You Should Avoid) →
- Serving Markdown version of pages to AI: Is it worth it? →
- Building a Multi-Agent System to Find My Next Neighborhood Abroad →
- Notion Blog with AI Translation →