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:
| Prefix | Expected policy | Healthy hit ratio (requests) |
|---|---|---|
/_astro/, /assets/ (hashed) | max-age=31536000, immutable | > 98% |
/pagefind/ fragments | immutable except entry file | > 95% |
| HTML (everything else) | max-age=0, or short + stale-while-revalidate | 80–95% |
/api/ (functions) | not cached | n/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.
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.
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.
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
| Measure | Before monitoring | After (6 months) |
|---|---|---|
| Hit-ratio incidents | 6 in 6 months | 5 in 6 months (2 more caught in CI) |
| Median time to detect | 9 days (via bill or ticket) | 16 minutes |
| Median incident duration | 11 days | 2 hours |
| Origin egress, monthly (docs site on S3) | 410 GB | 96 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.
Related
- Parent: Monitoring Static Sites in Production — the complete monitoring set.
- CDN Caching Rules for SSGs — the policy this monitor protects.
- Stale-While-Revalidate for Static HTML — raising HTML hit ratio safely.
- Cloudflare Pages Edge Caching Setup — where the cache rules are configured.
- Deploying a Static Site to S3 and CloudFront — the setup whose egress bill motivated this.