DEV Community

El_Necora
El_Necora

Posted on

Your Cloudflare analytics are rounded to the nearest 10, and the API will tell you so

If you run a small site on Cloudflare and you've ever squinted at the Web Analytics panel wondering why every number ends in a zero — 40 visits, 110 page views, 90 visits — it isn't a coincidence and the panel isn't broken. Cloudflare is showing you one event out of every ten, multiplied back up by ten.

That's fine at scale. At small scale it means the dashboard is rounding your traffic to the nearest ten and you can't tell the difference between a real change and the rounding.

Cloudflare does say in its docs that some analytics are "based on a sample". What the docs don't tell you is your sampling rate, when it kicks in, or how to check. The GraphQL API will tell you all three if you ask it.

Ask for sampleInterval

The RUM datasets expose a field called sampleInterval. It's the multiplier: 10 means Cloudflare kept one event in ten and multiplied the count by ten. 1 means it counted everything.

Almost nobody selects it, which is why almost nobody knows their numbers are estimates.

query($acc: String!, $site: String!, $s: Date!, $e: Date!) {
  viewer {
    accounts(filter: { accountTag: $acc }) {
      rumPageloadEventsAdaptiveGroups(
        filter: { siteTag: $site, date_geq: $s, date_leq: $e, bot: 0 }
        limit: 10000
        orderBy: [date_ASC]
      ) {
        count
        sum { visits }
        avg { sampleInterval }
        dimensions { date }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You need an API token with Account · Account Analytics · Read — nothing more, and it's worth creating a separate read-only one rather than reusing a deploy token. siteTag is the Web Analytics site tag, not the zone ID; you'll find it in the dashboard URL when you open the site's analytics.

curl -s https://api.cloudflare.com/client/v4/graphql \
  -H "Authorization: Bearer $CF_ANALYTICS_TOKEN" \
  -H "Content-Type: application/json" \
  -d @query.json
Enter fullscreen mode Exit fullscreen mode

What came back

This is a small site — a JPEG XL converter, a few thousand page views a month. Here's a slice of the daily output, measured on 28 August:

date          pageviews  visits   sampleInterval
2026-07-14           50      40             10.0
2026-08-11           40      10             20.0
2026-08-18          300     220             10.3
2026-08-21           10      10             10.0
2026-08-24           17      16              1.0
2026-08-25           13       9              1.0
2026-08-26            9       8              1.0
2026-08-27           12      11              1.0
2026-08-28           10      10              1.0
Enter fullscreen mode Exit fullscreen mode

Look at where the numbers stop being round. Everything from 24 August onward is real, counted event by event. Everything before it is one event in ten, multiplied back — and on 11 August, one in twenty.

Two things follow from that, and both of them bit me before I measured it.

Days with "no traffic" often had traffic. Across a 60-day window, 32 days showed no rows at all. That isn't a quiet weekend, it's the sampler not happening to keep anything. A day with 6 real visits has a decent chance of contributing zero sampled events.

Comparing two months is comparing two roundings. I had cheerfully reported an "18% drop" from one month to the next: 110 visits down to 90. In sampled terms that's 11 kept events versus 9. The entire "drop" was two events. There was no drop. I'd built a story on a rounding error.

The part I got wrong the first time

My first conclusion was that the sampling depended on how wide a window you asked for — 24 hours came back unsampled, 30 days came back at 10×, so obviously the query range was the trigger. The fix, I decided, was to request narrow windows and add them up.

That was wrong, and it's a nice example of a hypothesis that fits the data and still isn't the mechanism.

When I went back and requested seven-day windows across June and July — narrow windows, exactly as prescribed — they still came back at 10×. It isn't the width of the window. It's the age of the data. Cloudflare keeps raw events for a few days and samples everything older. In my zone the boundary sat somewhere between 21 and 24 August when measured on the 28th, so call it four to seven days.

Which means the "just ask for narrow windows" trick only ever worked because the days I was asking about happened to be recent. Same query today, over June, gives you the rounded numbers no matter how you slice it.

What to actually do about it

Select avg { sampleInterval } in every query. If it comes back as 1 you have real numbers. If it comes back as 10 you have an estimate with a granularity of ten, and you should not narrate small changes.

Trust the last few days for exact counts, and nothing older. If you need an exact figure for last month, you needed to have collected it last month.

At low traffic, use a source that doesn't sample. Google Search Console counts clicks and impressions one by one with no sampling at all. For me the two sources agreed once I compared like with like: GSC reported about 2.7 clicks a day from Google over those unsampled days, and the beacon independently saw 16 visits from google.com across five days. That agreement is only visible in the unsampled window.

Remember the beacon is JavaScript. Anything that blocks scripts blocks the count. On a site whose audience is developers, that's not a rounding error either — the sampled number is a floor, not a measurement.

If you want to see the shape of this on a real site, the one I measured is jpegxlconvert.com, a browser-side JPEG XL converter I wrote about here. Small enough that the sampling is impossible to ignore, which is exactly why it was worth measuring.

None of this is Cloudflare doing anything wrong — sampling is how you serve analytics for millions of zones without charging for it. The failure was mine: I read estimates as measurements for two months because the panel never showed me an error bar.

Has anyone found a documented figure for how long Cloudflare keeps raw RUM events? I measured four to seven days on one zone and I'd like to know whether that's a constant or whether it moves with load.

Top comments (0)