The raw number was 5x the truth
The traffic chart on the studio dashboard looked great this week. Too great. One project was showing daily "visit" counts in the tens of thousands, except nobody had done anything to earn numbers like that. They were real in the sense that the edge had served those requests, but they were lying about who was on the other end.
This is the thing nobody tells you about CDN analytics: httpRequests1dGroups counts everything. Bots, crawlers, API pings, health checks, the lot. The uniques field is distinct IPs including bots, so it's not a human count either. When I compared raw edge pageviews against the RUM beacon (which only fires client-side in a real browser), the edge was 4-5x inflated. Industry-corroborated, sure, but seeing it on your own data is different from reading about it.
Three lies, one pattern
The bot inflation wasn't just one problem. It showed up in three places, each wearing a different disguise.
1. Edge counts as "traffic"
The dashboard's primary KPI was raw edge pageviews. Bots were the majority of that number, and the chart made it look like traffic was climbing. The fix wasn't to find a better single number. It was to layer verification and label every tier honestly:
- Raw edge requests: kept on the chart but legend-hidden by default, labeled "incl. bots & app API, not humans"
- Edge visits (bot-partial): filtered to eyeball + HTML 200s, still includes headless browsers
- RUM beacon visits: fires client-side, so crawlers that don't execute JS are excluded; used as a cross-check
- Verified humans: a strict event requiring 8 seconds of dwell plus an interaction
Each tier is a stricter filter, and each is labeled for what it actually measures. No tier pretends to be something it isn't.
2. TikTok lifetime counters as daily flows
The social tiles had a different version of the same disease. The TikTok metrics coming back from the social API were account-lifetime counters: total views since the account existed. The dashboard was summing those into 30-day flow tiles as if they were period totals. A lifetime counter summed across a window doesn't give you the window's flow. It gives you a number that looks big and means nothing.
Worse, overlapping metrics were double-counted. Facebook page impressions plus media views both counted toward "views." Pinterest engagement plus pin clicks. And follower daily-gain series were charted as levels (absolute counts) instead of deltas, so the line told you nothing about growth.
The rebuild: one canonical metric per channel per category. Lifetime counters are split out and excluded from windowed totals. If accumulated daily snapshots exist, a lifetime counter yields a derived daily delta. Weekly-cadence channels (X reports weekly) stay off the daily axis entirely. A fine-note under the chart states every exclusion so the reader knows what's missing.
3. The crawler signature
One project's raw pageview count had the cleanest tell. When visitors roughly equal views, you're looking at crawlers. A human browsing a site generates several pageviews per visit. A crawler hits one page and leaves. Visitors ~= views is the crawler signature, and the raw $pageview event wore it openly for weeks.
The fix there was the strict human-signal event: 8 seconds of dwell time plus an interaction (scroll, click, anything). That event only fires for real people doing real things. Raw pageview-visitors got demoted to a context line on the chart, legend-deselected by default so they can't flatten the verified series.
Why not just use one good number?
Because every single number lies. Raw edge counts include bots. RUM beacons miss users with ad-blockers. Session analytics count someone who bounced in 2 seconds. The 8s-plus-interaction event is the strictest filter, but it's also the newest, so it has no history.
The transferable lesson: un-gated counters lie, and the fix is layered verification rather than a better single number. Show every tier, label what each one measures, and let the reader (or you, at 2am) compare them. When the verified-humans line diverges from the raw edge line, that gap IS the story. Hiding either number makes the gap invisible.
The flatten guard
One small thing that made a big difference: bot-scale series start legend-hidden, but only when they'd flatten the primary line. The threshold is 8x. If the raw series is 8x or more the primary human series, the primary would occupy less than 12% of the axis and become unreadable, so raw starts deselected. One legend click restores it.
// Series whose scale would flatten the primary human line start
// legend-deselected. One legend click restores any of them.
var primaryMax = hasVerified
? maxOf("verified")
: hasSessions ? Math.max(maxOf("visitors"), maxOf("appUsers"))
: maxOf("beaconVisits");
// 8x+ the primary means the primary occupies <12% of the axis
var edgeOff = maxOf("edgeVisits") > primaryMax * 8;
You can reuse the pattern: measure your primary series, measure your context series, and hide the context series when it would crush the axis. The reader can always toggle it back on. What they can't do is read a chart where the interesting line is 3 pixels tall.
What I'd do differently
I spent the first hour trying to find the one number that was "correct." There isn't one. The dashboard is more useful now that it shows four tiers with honest labels than it was when it showed one number that looked clean. The gap between tiers is where the insight lives.