Measuring Cloudflare Cache Hit Ratio per Host

Before the platform I work on moved to Fastly, it sat behind Cloudflare: dozens of Drupal sites, many of them in the same Cloudflare zone. The goal was to improve caching, and the first problem was measuring it properly.

The dashboard shows a "percent cached" figure per zone. That is fine when one zone is one site. But when a zone holds a dozen hosts, the zone number is dominated by the busiest one. A big site at 98% can hide three small ones at 40%. I needed one number per host, for any time window, for all hosts at once, in a format I could put in a spreadsheet and compare week to week.

So I wrote cloudflare-percent-cached, a small Node.js CLI. With per-host numbers to work from, the platform went from an 82% cache hit ratio to 96%, and origin requests dropped by roughly 78%.

The data is in the GraphQL Analytics API

Cloudflare exposes request analytics through a GraphQL endpoint. The dataset you want is httpRequestsAdaptiveGroups, which lets you filter by host and group by cache status. One query per host returns a handful of rows: how many requests had each status in the window.

query GetCachePerHost(
  $zoneTag: string
  $host: string
  $sinceTime: Time
  $untilTime: Time
) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      httpRequestsAdaptiveGroups(
        limit: 10000
        filter: {
          AND: [
            { datetime_geq: $sinceTime, datetime_leq: $untilTime }
            { requestSource: "eyeball" }
            { clientRequestHTTPHost: $host }
          ]
        }
      ) {
        dimensions { cacheStatus }
        count
      }
    }
  }
}

Two filters are worth explaining. clientRequestHTTPHost is what makes this per host instead of per zone. And requestSource: "eyeball" keeps only requests from real clients. Without it, requests made by Workers and other Cloudflare-internal sources are counted too, and the number stops matching what visitors experience.

The response is short. It looks something like this:

[
  { "dimensions": { "cacheStatus": "hit" },     "count": 912345 },
  { "dimensions": { "cacheStatus": "miss" },    "count": 40211 },
  { "dimensions": { "cacheStatus": "dynamic" }, "count": 18034 },
  { "dimensions": { "cacheStatus": "expired" }, "count": 6120 },
  { "dimensions": { "cacheStatus": "none" },    "count": 2410 }
]

Deciding what counts as "cached"

This is the part that matters more than the code. Cache hit ratio is a fraction, and you have to decide what goes on top and what goes on the bottom.

Rather than hard-code one opinion, the CLI takes both lists as options, with "hit" and "none" as defaults:

let total = 0;
let cached = 0;

for (const row of groups) {
  const status = row.dimensions.cacheStatus;
  if (include.includes(status)) cached += row.count;
  if (!exclude.includes(status)) total += row.count;
}

const percentCached = total > 0 ? (cached / total) * 100 : 0;

Whatever you choose, choose it once and keep it. A ratio is only useful if this week's number was computed the same way as last week's.

Using it

The CLI takes either a predefined window (30 minutes up to 30 days) or an exact start and end date. The exact dates turned out to be the most useful option. They let me compare the same week before and after a change instead of "the last 7 days" at two random moments.

# one host, last 24 hours
cfpc -z="<zone id>" -h="www.example.com" -t="24 hours"

# a fixed window, for before/after comparisons
cfpc -z="<zone id>" -h="www.example.com" \
  -s="2025-01-01T00:00:00Z" -u="2025-01-31T23:59:59Z"

# every site in a CSV, results to another CSV
cfpc -f="sites.csv" -t="7 days" -o="results.csv"

For the whole platform, a CSV lists every zone and host:

zoneId,host
<zone id 1>,www.brand-one.com
<zone id 1>,www.brand-two.com
<zone id 2>,www.brand-three.com

The output CSV has the hit ratio, miss ratio, total requests and cached requests per host. Sorting it by total requests and then by hit ratio gives you a list ordered by where an improvement saves the most origin traffic.

What the numbers showed

Per-host numbers changed the conversation. Instead of "caching is at 82%", it became "these few hosts are well below the rest, and here is how much origin traffic they cause". That turns a vague goal into a short list of concrete things to look at, one host at a time, and each fix shows up in the next week's CSV.

The same habit carried over when the platform moved to Fastly. I wrote about that in Migrating a CDN from Cloudflare to Fastly.

If you build your own

← All posts