Bot traffic on a small site: measure before you block
A practical way to separate crawlers, broken clients and abuse using request logs, rate patterns and a clear response policy.
A busy access log does not tell you who is reading. One crawler can generate more requests than a thousand human visitors, while a single address can represent many people behind a shared network. Begin with a measurement window and a question: what resource is actually under pressure?

1. Establish a baseline
Measure requests per minute, distinct paths, error rate, response size and bandwidth over a representative week. Segment by status code and user agent, but do not treat the user-agent string as proof of identity. Compare a normal period with the suspected spike. If the site stays responsive, the cost may be more important than the request count.
2. Classify behavior, not labels
A bot may fetch robots.txt and a few changed pages; a scraper may sweep the entire archive repeatedly; a broken client may loop on one missing URL. Look for path sequences, intervals and concentration. A repeated 404 on an old page suggests a link or redirect problem, not automatically a hostile actor.
Many distinct paths, slow pace → check crawl policy and cache.
One path, high frequency → investigate client or redirect.
High rate plus errors or load → rate limit or challenge carefully.
3. Choose the lightest intervention
Cache static assets, fix accidental infinite redirects and return an honest 404 for missing pages. Use robots.txt to state a crawl preference. RFC 9309 makes clear that robots rules are requests to crawlers, not access control. When necessary, apply rate limits or challenges to the observed pattern, then check that legitimate readers and search crawlers still reach the site.
4. Measure the result
Record the rule, start time, target path and expected change. Compare origin load, error rate and real page delivery afterward. Revert a rule that reduces legitimate access without meaningfully reducing cost. Keep logs only as long as operationally necessary and avoid publishing raw IP addresses.
Caddy's log directive enables HTTP access logging and supports JSON output. Verify what your current configuration records before making claims about bot share or bandwidth.
Count a synthetic Nginx combined sample
The article above discusses Caddy. This separate exercise uses Nginx’s predefined combined layout; Caddy JSON and custom log formats are different inputs. It counts status classes and known response-body bytes, not crawler identity or server cost.
The default four rows are invented, using documentation-only addresses. Use only synthetic or anonymised records. The parser does not redact what you paste. It keeps input in this page, sends and saves none of it, and excludes addresses, paths, referrers and agents from the summary.
This parser needs JavaScript. Its controls stay unavailable until it loads. The synthetic example and worked totals remain readable.
The strict supported layout is: address, dash, one username token, bracketed common-log timestamp, quoted request, status 100–599, body-byte count, quoted referrer and quoted agent. Default hexadecimal escapes such as \x22 are accepted inside quoted fields. JSON escaping, extra columns, internal blank rows and usernames with literal spaces are not supported. Unsupported input rejects the whole batch, with row numbers only; no partial total is presented as complete.
A dash byte value is unknown, not zero. The synthetic example has 4 responses, one in each of 2xx–5xx, 1,200 known body bytes and one unknown byte count. Nginx combined uses body_bytes_sent; it does not measure full wire traffic, CPU or request duration. Safe-integer limits are checked. Counting responses does not deduplicate people, authenticate a User-Agent or establish abuse.
Primary reference: Nginx log module: combined layout and escaping. This is original browser code, tested with synthetic fixtures; no production access log or server configuration is read or changed by it.
Sources & scope
These sources support the technical principles above. Commands and compatibility can change; verify the current version before changing a machine.
This is this website. Its articles are original work inspired by the domain's technical history; we are not the former author. Meet the author.
Many requests and costly requests are different questions
The combined-log parser above can count responses and known body bytes. It cannot recover processing time that was never logged, identify a person from a User-Agent, or attribute CPU cost to a crawler.
- Fix one observation window and the exact log format. Keep only the fields needed for the comparison; do not publish raw visitor identifiers or query strings.
- Compare request count, known response bytes and status groups separately. Preserve missing bytes as unknown rather than zero.
- If the existing log includes
$request_time, compare durations on the same routes and window. Nginx measures elapsed request processing through sending the response; this is not CPU time. - Check cache and upstream evidence, if actually recorded, before attributing a bottleneck. Without it, write an investigation question rather than a blocking rule.
Synthetic comparison: group A has 100 requests, each 0.020 seconds; group B has ten requests, each 2.000 seconds. Their summed durations are 2 and 20 seconds respectively. B has fewer requests yet a larger sum. Concurrent requests overlap: 22 summed seconds is not 22 seconds of wall-clock delay and does not measure processor consumption.
Use a record: window __; grouping rule __; route __; requests __; known/unknown bytes __; timing field present __; duration summary __; unanswered cause __. The existing combined sample lacks timing, so its timing result must remain unavailable. These invented groups are not claims about a named bot or this server.
Source: nginx.org.
Choose between crawler instructions, access control and rate limits
Start with the outcome you need. “Please do not crawl this path”, “only authenticated users may read it” and “slow down these requests” are three different requirements. A user-agent label does not prove identity. Use the following fictional response card to review a policy before changing a production configuration.
- Public material, excluded from cooperative crawling: publish the intended robots rule, then request the path directly in your own test environment. A normal public response can still be 200. If the path contains private information, this design has failed the privacy requirement even if a cooperative crawler obeys it.
- Protected material: an unauthenticated test request must not receive its body. A 401 response challenges the client and must include an applicable
WWW-Authenticateheader. Check the response body as well as the status. A login-looking page that returns confidential content elsewhere has not established protection. - Excessive request rate: record the controlled test boundary, counting window and observed response. A 429 response means too many requests; it may include
Retry-After. It does not establish the caller’s identity, and a missing 429 does not prove that a server has no rate control.
Worked policy review: a fictional public guide is excluded in robots.txt but direct GET returns 200 with the guide. That matches a crawling preference, not a confidentiality policy. A separate private draft responds 401 with a challenge and no draft text: this passes only the unauthenticated-denial case, not the complete login lifecycle. A local rate-limit test responds 429 and Retry-After: 60; the client pauses rather than retrying repeatedly. The 60 seconds are an invented example, not a recommended universal threshold.
Record to copy: requirement ___; own test URL ___; test identity or anonymous ___; controlled request count/window ___; status ___; relevant response headers ___; sensitive body absent/present ___; next permitted request time ___; result and remaining tests ___. Use synthetic paths and keep credentials and raw visitor identifiers out of the record.
These are response expectations, not a claim that this website deployed a new authentication or rate-limiting system. Test only systems you control and avoid production load tests without an agreed limit. The standards explain the separation: RFC 9110 §15.5.2 specifies 401, while RFC 6585 §4 defines 429.
RFC 9309, section 3: robots rules are not access authorisation