Cloudflare Web Analytics counts a visit whenever a browser runs the beacon, and a lot of browsers that run it are not readers. The usual culprits are the single fetch a social network makes to draw link previews, your own headless-browser checks, and a staging site that shares the same token. Next to that, the table can also be missing real readers: one automatic setup option never injects the beacon for visitors in the EU. To get a reader count you subtract the first group, check the setting for the second, and only then add up the rest. The rows below come from our own sites, and the model test at the end shows how easily the sum goes wrong.
Method
The raw material is what we watched on our own Cloudflare sites from mid-August to late September 2026: dashboards, zone request logs pulled through the GraphQL API, and Content Security Policy headers tried in a real browser. Every finding went into a dated notes file with its source, and the few claims about Cloudflare's behaviour were compared with Cloudflare's documentation.
From those findings we made 21 test situations. Each looks like a chat message from an owner: a small table, a question, and a request to reply in JSON. Fourteen are traps, where the first reading you would reach for is wrong. The other seven are controls, in which real readers or real errors have to survive, so a model that deletes too eagerly gets caught. Only the values in the JSON were scored, not the reasoning beside them. Both models, Sonnet and Haiku, took each situation twice, with our skill loaded and with nothing loaded, one run per pairing.
Results
What looked like readers and was not
On one of our Bulgarian news sites the dashboard showed 211 views for a week. Of those, 105 came from headless Chrome, which was our own monitoring. Another 84 carried a social-network referrer, arrived from the US, Sweden and Ireland, were one view per visit, and landed in the hour each article was published. That pattern is a preview fetch: the network requests the page a single time to build a thumbnail. Not one view came from Bulgaria, the country the site is written for.
The fix is not to drop a referrer wholesale. A visit with the same referrer stays in if the visitor sits in the audience country, has several views, and is spread over days instead of bunching at publishing hours. One of our controls is exactly that, and a careless rule fails it.
What looked like errors and was not
OpenNext on Workers keeps a regional cache and talks to it through a made-up host name. In zone analytics that host answers 504 for a miss, 200 for a hit and 204 for a write, so a filter that counts every 5xx in the zone reports each cache miss as a server error. A second group of rows is the Worker's own outbound calls and Cache API calls, which show up in the zone figures of the site it serves. On one of our Workers sites they were about a third of all rows.
Zone analytics also count crawlers, scripts and scans, and work without any beacon, which is why they run higher than Web Analytics for the same site.
What was missing
One of the automatic setup choices omits EU visitors, and its documentation page states that the snippet is left out for them (Cloudflare setup guide). Bulgaria is a member state. One of our sites ran with it for 18 days, and its Bulgarian readers were missing from the dashboard for all of them. Two further causes of an empty dashboard sit in the Content Security Policy: a policy needs two entries, the host that serves the script under script-src and the host that receives reports under connect-src, and the word 'self' provides neither.
Sampling is the last trap. Cloudflare says unsampled beacon data is kept for 7 days and then aggregated to around 10 percent (Cloudflare FAQ). We read older figures as estimates for that reason: a figure of 10 there may stand for a single beacon.
A slip of our own
On 17 October 2026 the first version of our script that counts people on our own site failed in just this way. It filtered the social-network robot by country name, but Cloudflare's countryName field holds a two-letter code such as "US". The filter matched nothing, no error appeared, and the robot was counted as people until we looked at the raw values. A zero from a filter proves nothing until the same filter has been seen to catch something.
The model test
Across the 21 situations, bare Sonnet was right in 14 and bare Haiku in 11. Loading the skill lifted them to 21 and 20.
Bare Sonnet's seven misses are worth reading. Given the mixed dashboard of 203 views it removed only the headless ones and answered 139 readers, explaining that the owner shares each article and the foreign visits were diaspora; the right answer was 70. It read the plain EU-exclusion case correctly, but in a case that paired the setting with preview fetches from the US, Sweden and Ireland it again counted the 84 previews as readers, where the answer was 0. In the control with 282 genuine readers, it threw out 33 real Bulgarian readers from the social app as automated and answered 249. It said a separate API token was needed when the wrangler login was enough, returned no number for the beacons behind a sampled figure, and twice called a small recent count "not exact" where the 7-day rule makes it exact.
Haiku without the skill missed ten. Five were Sonnet's misses too: the 84 previews, the token, the 249 and the two small recent counts. The rest were its own. It answered 115 cache hits where the answer was 40, which is the hits plus the writes. In a second control it dropped 14 of 54 genuine social-network readers. It called the dashboard incomplete for a site set to collect every visitor, gave 10 instead of 1 for the beacons behind a sampled figure, and once returned JSON that did not parse.
Sonnet did already get the cache host, the Worker rows, both Content Security Policy cases and the staging site right, so its weakness is not general. It lies in social previews (twice), sampling, the token question and over-correcting on controls. With our traffic-reality-check skill loaded, Haiku's one miss was a control: faced with 12 clean page views, it called the figure imprecise since the beacon cannot see people who block scripts.
What we did not measure
- Only Cloudflare. We did not test any other analytics product, and a different tool may count previews differently.
- The situations are ours. We wrote all 21 from our own sites, so the tests fit traps we already knew. Dashboards from other owners might hold traps we never met.
- One run per model and situation. We did not repeat runs, so we have no estimate of variance, and a single changed answer would move a score by one.
- Values only. The score ignores the explanation. A right number with a wrong reason counts as a pass.
- Sampling is simplified. Cloudflare's FAQ also says a sampling level between 0.0001 and 100 percent is chosen dynamically when you query, and that very small sites are sampled more. We treated data inside the 7 days as exact, as our notes did, and did not test that wording again.
- Documentation moves. The GraphQL limits page says adaptive datasets keep at least 31 days, while we measured a reach of at least 20 days in September. Both can be true as floors, and the page can change; where it differs from this post, trust the page.
Read this post as Markdown: /blog/cloudflare-web-analytics-real-readers.md · Atom feed.
