Alerting on Cache Hit Ratio Drops

A static site's speed and cost both depend on one number most teams never watch: the fraction of requests the CDN answers from its cache. At 97% hits, readers get edge-local responses in tens of milliseconds and the origin — whether an object store or a VM — handles a trickle. At 60%, readers far from the origin wait hundreds of milliseconds more, the origin sees ten times the requests, and on metered storage the egress bill climbs. The site keeps working throughout, so uptime checks stay green and nobody notices until the invoice or a support ticket arrives.

This guide measures hit ratio by path prefix from CDN analytics, alerts on sharp drops compared with the same hour last week, and walks through the four causes behind almost every drop. It is part of Monitoring Static Sites in Production.

Prerequisites

  • Access to the CDN's analytics API or logs with cache status per request (Cloudflare GraphQL Analytics, CloudFront logs with x-edge-result-type, Fastly real-time stats).
  • A scheduler that can run a query every fifteen minutes — a scheduled GitHub Actions workflow or a Cloudflare Worker cron trigger.
  • The cache policy you intend to have, written down, from CDN Caching Rules for SSGs.

Step 1: Measure by Prefix

A site-wide hit ratio blends hashed assets (which should almost always hit) with HTML (which revalidates often). A drop in one can be masked by the other. Group by the prefixes that have different policies:

PrefixExpected policyHealthy hit ratio (requests)
/_astro/, /assets/ (hashed)max-age=31536000, immutable> 98%
/pagefind/ fragmentsimmutable except entry file> 95%
HTML (everything else)max-age=0, or short + stale-while-revalidate80–95%
/api/ (functions)not cachedn/a — exclude

On Cloudflare, the GraphQL Analytics API returns request and byte counts by cache status; a Worker cron trigger queries it every fifteen minutes:

const q = `query($zone: String!, $since: Time!, $until: Time!) {
  viewer { zones(filter: {zoneTag: $zone}) {
    httpRequestsAdaptiveGroups(limit: 1000, filter: {datetime_geq: $since, datetime_lt: $until}) {
      count sum { edgeResponseBytes }
      dimensions { cacheStatus clientRequestPath }
    } } } }`;
// group results by prefix: hit = cacheStatus in ('hit','stale','revalidated','updating')

On CloudFront, the equivalent is an Athena query over standard logs grouping by x_edge_result_type (Hit, RefreshHit, Miss) and a prefix expression on cs_uri_stem.

Why a site-wide ratio hides problems Three prefixes on a normal day: hashed assets at 99 percent hits, HTML at 91 percent, search fragments at 97 percent, averaging 97 percent overall. On the incident day, HTML dropped to 52 percent while assets stayed at 99 percent, and the site-wide average fell only to 88 percent because assets dominate request volume. Hit ratio by prefix, normal day vs incident day normal day incident day assets 99% 99% HTML 91% 52% search 97% 97% site-wide 97% 88% — looks tolerable Assets are ~70% of requests, so a collapse on HTML moves the blended number only modestly
Per-prefix ratios turn "a bit lower today" into "HTML caching is broken", which is the level of detail needed to fix it.

Step 2: Alert on Change Against Last Week

Hit ratio has daily and weekly rhythm — lower overnight when traffic is thin and more requests find cold edges, higher at peak. Compare each fifteen-minute bucket with the same bucket seven days earlier, and alert when a prefix with meaningful traffic falls sharply:

const drop = (now, lastWeek) => lastWeek - now;
for (const p of prefixes) {
  if (p.requests < 2000) continue;                    // too little traffic to judge
  const d = drop(p.hitRatio, p.hitRatioLastWeek);
  if (d > 0.15 && p.hitRatio < 0.85) alert(`${p.prefix}: ${pct(p.hitRatio)} vs ${pct(p.hitRatioLastWeek)} last week`);
}

Require the condition in two consecutive buckets to ignore the dip right after a deploy's purge, which on this site lasted about twenty minutes before the edges refilled.

Week-over-week comparison ignores normal rhythm Two lines over one day of HTML hit ratio: last week and today. Both dip overnight to about 80 percent and rise to about 93 percent at peak. A fixed 85 percent threshold would fire every night. Today's line falls to 52 percent in the afternoon while last week stays at 92, and only that gap triggers the alert. HTML hit ratio: today vs same day last week 40% 100% fixed 85% line: fires nightly 52% vs 92% → alert night dip: both weeks 00:00 24:00
The overnight dip is normal and shared by both weeks; the afternoon gap is the incident.

Route the alert to the team channel; escalate to a page only if a high-traffic prefix stays below 50% for thirty minutes, because at that point origin load becomes a reliability risk rather than a cost.

Step 3: Diagnose the Four Usual Causes

Nearly every hit-ratio incident on the sites measured came from one of four configuration changes:

A new Vary header. Vary: Cookie or Vary: User-Agent makes the CDN store a separate copy per distinct value, so almost every request misses. It usually arrives with a new edge function, an A/B testing tool, or a header rule copied from an application server. Check with curl -sI for vary on an affected URL.

A cookie or query string in the cache key. Marketing links with utm_* parameters, or a consent cookie read by an edge function, fragment the cache when the CDN includes them in the key. Configure the cache key to ignore query strings on static paths (or allow-list the few that matter) and to ignore cookies entirely for static content.

A Cache-Control change on a large set of files. A header rule meant for HTML that also matched hashed assets, or a build tool upgrade that changed asset paths so the immutable rule no longer matched. Check the cache-control header on one hashed asset and one HTML page.

Purge everything on every deploy. A deploy script that purges the whole zone instead of only HTML empties the cache of assets that did not change. See Purging the CDN Cache After a Static Deploy.

Causes of eleven hit-ratio incidents Horizontal bars counting incidents by cause across four sites over a year. A new Vary header caused 4, cookies or query strings in the cache key caused 3, Cache-Control changes on the wrong files caused 3, and purge-everything deploys caused 1. Eleven incidents, four causes, all configuration new Vary header 4 cookie / query in cache key 3 Cache-Control on wrong files 3 purge everything on deploy 1 Four production static sites, twelve months; median detection 16 minutes with alerting
None of the eleven was caused by traffic; every one was a change someone made, which is why alerting on change works.

Step 4: Prevent the Common Causes in CI

Three of the four causes are visible before deploy. A header check against the preview deploy — part of the smoke tests from Running Smoke Tests Against a Preview URL — can assert that a hashed asset returns immutable with a one-year max-age, that HTML returns the intended short policy, and that no response carries Vary beyond Accept-Encoding. On the site where the most incidents occurred, adding those three assertions caught two further misconfigurations in pull requests over the following six months.

Tie Alerts to Changes

A hit-ratio alert is far faster to resolve when it arrives with the list of changes made in the preceding hour. Record every deploy, CDN configuration change and header-rule edit as an event — a line in a shared log or annotations on the dashboard — and have the alert include the events from the last sixty minutes. In ten of the eleven incidents above, the cause was among the changes listed in the alert, and the on-call person reverted it without further investigation.

Measured Impact

MeasureBefore monitoringAfter (6 months)
Hit-ratio incidents6 in 6 months5 in 6 months (2 more caught in CI)
Median time to detect9 days (via bill or ticket)16 minutes
Median incident duration11 days2 hours
Origin egress, monthly (docs site on S3)410 GB96 GB
Monitoring cost< 1 USD (Worker cron + API queries)

The egress change is the business case in one line: on the S3-backed docs site, the longest undetected incident had quadrupled origin egress for eleven days.

Pitfalls & Rollback

  • Site-wide ratio only. Assets dominate request counts and mask HTML problems. Measure by prefix.
  • Fixed thresholds. Normal daily rhythm makes them noisy; compare against the same time last week.
  • Counting function responses. Uncached API routes drag the ratio down and are expected to miss; exclude them.
  • Alerting during deploy purges. Require two consecutive bad buckets so the post-deploy refill does not fire.
  • Rollback: the monitor is a scheduled query; disabling it has no effect on the site. The fixes it prompts — removing a Vary, adjusting a cache key — are ordinary configuration changes reverted the same way.

Conclusion

Cache hit ratio is the static-site metric that fails silently. Measuring it per path prefix, comparing each quarter-hour with the same time last week, and alerting on sharp drops caught configuration mistakes in a median of sixteen minutes instead of nine days, cut incident duration from days to hours, and reduced origin egress by three quarters on the S3-backed site. Most of the causes — a new Vary, a fragmented cache key, a mis-targeted Cache-Control rule — can also be asserted against every preview deploy before they ship.

FAQ

What is a healthy cache hit ratio for a static site?

For hashed assets, above 98 percent by requests. For HTML with short cache lifetimes, 80 to 95 percent depending on traffic and revalidation settings. Overall, well-configured static sites usually sit between 90 and 98 percent.

Should I measure by requests or by bytes?

Both. Request ratio shows how often the origin is contacted, which drives latency and origin load. Byte ratio shows how much data the origin serves, which drives egress cost. A drop in either matters.

Why alert on change rather than a fixed threshold?

Hit ratio varies with traffic patterns, time of day and content type. A fixed threshold either fires constantly or misses real problems. Comparing each hour with the same hour a week earlier catches configuration changes while ignoring normal variation.

What usually causes a sudden drop?

A new Vary header, a cookie or query string that becomes part of the cache key, a Cache-Control change on a large set of files, or a deploy that purged the whole cache. Most are configuration changes rather than traffic changes.