Seatext library / BotRefund evidence
Can I See Bot Visits in My Server Logs? A Practical Guide to Log Analysis
Yes, server logs reveal bot activity through request frequency, user-agent strings, IP patterns, and behavioral anomalies. This guide walks you through extracting and interpreting those signals with a ready-to-use console script.
✓ Built for advertisers who need clear, refund-ready traffic evidence.
Yes, you can see bot visits in your server logs. Every request leaves a line with the IP address, timestamp, HTTP method, URL, status code, and user-agent string. Bots often betray themselves through high request rates, missing or suspicious user agents, repetitive paths, and IP addresses that don't match human browsing patterns. Below is a step-by-step process to pull those signals out of raw logs, plus a console script you can run today.
What server logs actually show you
Access logs (Apache, Nginx, IIS) record one line per HTTP request. The combined log format includes:
- Client IP — the source address; bots often cluster in hosting ranges or residential proxy pools.
- Timestamp — down to the second; bots can fire dozens of requests per second.
- Request line — method, path, protocol; bots hammer specific endpoints (login, search, API).
- Status code — 200, 404, 403, 429; a spike in 404s or 429s often means a scanner.
- Bytes sent — unusually small or large payloads can indicate headless browsers skipping assets.
- Referrer — often empty or spoofed for automated traffic.
- User-Agent — the most visible clue; bots may use generic strings ("python-requests/2.31"), outdated browsers, or copy-pasted Chrome headers that don't match other fingerprints.
Error logs add context: upstream timeouts, PHP fatal errors, or WAF blocks triggered by the same IPs.
Prerequisites before you start
- Log access — SSH to the server, or download logs via SFTP / cloud console (AWS CloudWatch, GCP Logging, Azure Monitor).
- Time window — pick a 24–72 hour slice; longer windows dilute spikes, shorter ones miss low-and-slow crawlers.
- Tooling —
awk,grep,sort,uniqon Linux/macOS; PowerShellSelect-Stringon Windows. The console script below works in any browser dev-tools console or Node.js. - Baseline — know your normal: average requests/minute, top 10 IPs, top 10 paths, typical user-agent distribution.
Step-by-step process to parse logs for bot activity
1. Extract the fields you need
# Apache/Nginx combined format
awk '{print $1, $4, $5, $6, $7, $8, $9, $10, $11}' access.log | head -20
This prints IP, timestamp, request, status, bytes, referrer, user-agent. Adjust field numbers if your format differs.
2. Count requests per IP
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -30
IPs with thousands of requests in an hour warrant inspection. Cross-reference with known CDN/proxy ranges (Cloudflare, Fastly, AWS ALB) — those IPs are shared, so look at the X-Forwarded-For header instead.
3. Spot suspicious user agents
awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -nr | head -30
Flag entries that:
• Contain "bot", "crawler", "spider", "scraper", "python", "go-http", "curl", "wget"
• Claim Chrome 120 but lack sec-ch-ua headers (visible only in full header logs)
• Are empty or just "-"
4. Find high-frequency endpoints
awk -F'"' '{print $2}' access.log | awk '{print $2}' | sort | uniq -c | sort -nr | head -20
Login, registration, password-reset, search, and API endpoints are favorite targets. A sudden surge on /wp-login.php or /api/v1/checkout is a red flag.
5. Correlate status codes with IPs
awk '$9 ~ /^4/ {print $1, $9}' access.log | sort | uniq -c | sort -nr | head -20
Many 403/429/500 from the same IP suggests a blocked or rate-limited bot.
6. Run the console log parser
Paste this into your browser dev-tools console (or save as parse-logs.js and run with Node). It accepts pasted log lines and returns a summary table.
function parseLogLines(raw) {
const lines = raw.trim().split('\n').filter(l => l.length);
const ipCount = {};
const uaCount = {};
const pathCount = {};
const statusCount = {};
const ipUa = {};
const combinedRegex = /^(\S+) \S+ \S+ \[(.*?)\] "(\S+) (\S+) HTTP\/\d\.\d" (\d{3}) (\d+) "(.*?)" "(.*?)"$/;
lines.forEach(line => {
const m = line.match(combinedRegex);
if (!m) return;
const [, ip, , method, path, status, , , ua] = m;
ipCount[ip] = (ipCount[ip] || 0) + 1;
uaCount[ua] = (uaCount[ua] || 0) + 1;
pathCount[path] = (pathCount[path] || 0) + 1;
statusCount[status] = (statusCount[status] || 0) + 1;
if (!ipUa[ip]) ipUa[ip] = new Set();
ipUa[ip].add(ua);
});
const top = (obj, n=15) => Object.entries(obj).sort((a,b)=>b[1]-a[1]).slice(0,n);
console.table(top(ipCount).map(([ip,count])=>({IP:ip, Requests:count, UniqueUAs:ipUa[ip].size})));
console.table(top(uaCount).map(([ua,count])=>({UserAgent:ua.slice(0,80), Count:count})));
console.table(top(pathCount).map(([path,count])=>({Path:path, Count:count})));
console.table(Object.entries(statusCount).map(([status,count])=>({Status:status, Count:count})));
// Heuristic flags
Object.entries(ipCount).forEach(([ip,count]) => {
if (count > 500 && ipUa[ip].size === 1) console.warn(`⚠ ${ip}: ${count} requests, single UA — likely bot`);
if (count > 1000) console.warn(`⚠ ${ip}: ${count} requests — high volume`);
});
}
// Usage: paste log lines between the backticks
parseLogLines(`
192.168.1.1 - - [12/Aug/2026:10:00:00 +0000] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0..."
10.0.0.5 - - [12/Aug/2026:10:00:01 +0000] "POST /login HTTP/1.1" 401 567 "-" "python-requests/2.31"
...`);
The script builds frequency tables for IPs, user agents, paths, and status codes, then flags IPs with high volume and only one user agent — a classic bot signature.
Key patterns that signal automated traffic
| Pattern | What it looks like in logs | Why it matters |
|---|---|---|
| Superhuman request rate | > 60 req/min from one IP, sustained | Humans browse slower; this matches headless browser loops |
| Single user agent per IP | Thousands of requests, identical UA string | Real browsers send varying headers (accept-language, encoding) |
| Missing referrer on deep links | Direct hits to /checkout or /api/lead with "-" referrer | Bots skip navigation; humans arrive via internal links |
| Sequential ID enumeration | /user/1001, /user/1002, /user/1003 in seconds | Scrapers walk numeric IDs; humans don't |
| Static asset avoidance | HTML requests only; no CSS, JS, images, fonts | Headless browsers often disable resource loading to save bandwidth |
| Uniform timing | Requests spaced exactly 1.0s or 0.5s apart | Scripted sleep() loops; human intervals are jittery |
BotRefund's detection engine treats each of these as independent evidence, then cross-checks them against browser, network, device, and behavior signals before scoring a visit. A single anomaly is never a verdict — privacy tools, corporate proxies, and unusual devices can mimic bot patterns for genuine users.
Common mistakes when reading logs
- Blocking by IP alone. Residential proxy networks rotate IPs per request; you'll block legitimate users sharing the same exit node.
- Trusting user-agent strings. Bots spoof Chrome headers perfectly. The Console Debug Evaluator check looks for mismatches between the claimed UA and actual browser API behavior — automation tools often patch APIs in ways that break under cross-examination.
- Ignoring CDN/proxy headers. If you're behind Cloudflare, the real client IP is in
CF-Connecting-IPorX-Forwarded-For. Log the original IP, not the CDN edge IP. - Treating all bots as malicious. Googlebot, Bingbot, GPTBot, and monitoring services (Pingdom, UptimeRobot) are beneficial. Identify them via reverse DNS or published IP ranges before filtering.
- Sampling too small a window. Low-and-slow bots make 5 requests/hour across 1,000 IPs. You need 7+ days of logs to see the pattern.
Verification: how to confirm your findings
- Reverse DNS lookup on flagged IPs:
dig -x 1.2.3.4. Hosting providers (aws, digitalocean, linode, vultr) and proxy services (brightdata, oxylabs, smartproxy) appear in PTR records. - Check ASN ownership via
whois -h whois.cymru.com " -v 1.2.3.4". Data-center ASNs = higher bot probability. - Replay a sample request with
curl -v -A "flagged-UA" -H "Referer: " https://yoursite.com/flagged-path. Does the server respond differently? Does a WAF block it? - Correlate with analytics — GA4/ Matomo sessions from the same IP/UA should show near-zero engagement (no scroll, no clicks, < 1s dwell). BotRefund's behavioral signals (ghost clicks, absent mouse tremor, superhuman input speed <1ms, grid-aligned movements) are client-side counterparts to these log patterns.
- Submit a refund claim if the bot clicked your Google/Meta ads. BotRefund captures video proof per click and negotiates with ad platforms; customers have recovered spend dating back to 2017.
Limitations of log-only analysis
- No browser fingerprint. Logs don't reveal canvas hash, WebGL renderer, font list, or audio context — signals that separate headless Chrome from real Chrome.
- No behavioral data. Mouse tremor, click latency, scroll depth, and form interaction speed live in the browser, not the access log.
- Encrypted traffic hides payloads. POST bodies (form data, JSON) are absent from standard access logs; you need application-level logging or a WAF to see them.
- Shared IPs obscure identity. CGNAT, corporate VPNs, and residential proxies put hundreds of users behind one IP. Log analysis alone cannot distinguish them.
- Log rotation and retention. Default configs keep 7–30 days. Long-term trend analysis requires centralized logging (ELK, Splunk, Datadog, or cloud logging).
For a complete picture, combine log analysis with client-side detection. BotRefund runs 106 independent checks — including the Console Debug Evaluator — and feeds every signal into an AI model that weighs the full pattern, achieving 99% accuracy by corroboration, not single tells.
Key facts
| Fact | Detail | Source |
|---|---|---|
| Bot click impact | Up to 20% of Google and Meta ad budgets lost to bot clicks | S2 |
| Detection signals | 106 independent checks across browser, network, device, behavior | S1 |
| Accuracy method | Cross-checked context + AI prediction, not single rules | S1 |
| Reported accuracy | 99% by corroborating complete pattern | S1 |
| Setup time | About one minute to add to website | S2 |
| Refund lookback | Google Ads spend dating back to 2017 recoverable | S2 |
| Behavioral signals | Ghost clicks, honeypot traps, robotic mouse, absent tremor, superhuman speed (<1ms), grid-aligned paths, static sessions, unnatural durations | S2, S6, S7 |
| Case study result | FinTrust recovered $140,000, 14% bot click rate, +18% conversion rate | S4 |
| Affiliate fraud vectors | Headless browsers, CAPTCHA solving, spoofed data, residential proxies | S5 |
| Ad fraud trends | AI-powered telemetry, residential proxy botnets, behavioral emulation | S8 |
FAQ
Can I identify specific bots by name from logs?
Only if they declare themselves in the user-agent (e.g., "Googlebot/2.1", "GPTBot/1.0"). Most malicious bots spoof common browser strings. Use reverse DNS and ASN lookups to infer bot families.
How far back should I keep logs for bot analysis?
Minimum 30 days; 90 days lets you spot seasonal campaigns. Configure log rotation to ship older files to cheap object storage (S3, GCS, Blob) instead of deleting.
What's the difference between a crawler and a malicious bot in logs?
Crawlers obey robots.txt, crawl at polite rates, identify honestly, and come from known IP ranges. Malicious bots ignore robots.txt, hammer endpoints, spoof headers, and originate from hosting/proxy ASNs.
Should I block IPs that show bot patterns?
Block at the WAF or application layer with a challenge (JS challenge, CAPTCHA) rather than a hard drop. Hard blocks catch real users behind shared IPs. BotRefund suppresses conversion events for automated signals so ad platforms retrain on verified humans.
Can server logs show bots that execute JavaScript?
Only if the bot loads the page and triggers the same requests a browser would (analytics pixels, API calls). Headless browsers that fully render appear nearly identical to humans in access logs — you need client-side fingerprinting to catch them.
How do I automate this analysis daily?
Ship logs to a SIEM or run a cron job that executes the parser script, stores summaries in a time-series DB (InfluxDB, TimescaleDB), and alerts when IP request count or error rate exceeds your baseline thresholds.
What if my logs are in JSON format?
Adjust the regex in the console script to parse JSON fields (e.g., json.remote_addr, json.request, json.http_user_agent). The same frequency logic applies.
Further reading and comparison sources
These external sources provide additional context for evaluating the topic. Their inclusion is not an endorsement.
Learn more
Visit the website for more information.