Features Pricing Regions Support Blog ⚡ Free Audit Start free Log in FR
← Back to blog

How to Measure Your Cache Hit Ratio (And What to Do When It's Low)

Cache hit ratio is the single most useful metric for understanding your cache's health. Here's how to measure it across different stacks and what numbers to aim for.

How to Measure Your Cache Hit Ratio (And What to Do When It's Low)

What is cache hit ratio?

Cache hit ratio is the percentage of HTTP requests served directly from cache, without hitting your origin server. A hit ratio of 90% means 9 out of 10 requests never reach your PHP application or database.

Hit ratio = (cache hits) / (cache hits + cache misses) × 100

A high hit ratio means:

  • Faster responses for users (cached responses are 10–100× faster than origin responses)
  • Lower server load (fewer requests reach PHP/database)
  • Better Core Web Vitals scores (TTFB is dominated by origin response time on misses)

Measuring cache hit ratio by stack

Cloudflare

The easiest place to start. In the Cloudflare dashboard:

  1. Go to Caching → Cache Analytics (or Analytics & Logs → Traffic)
  2. Look at the cache status breakdown (hit / miss / expired …)

Cloudflare shows requests split into: hit, miss, expired, bypass, revalidated, dynamic.

For a rough hit ratio: hit / (hit + miss + expired). Ignore bypass (requests configured to skip cache) and dynamic (Cloudflare Workers or API responses).

Via the API (the legacy Zone Analytics endpoint was sunset in 2020 — use the GraphQL Analytics API):

SINCE=$(date -u -d '7 days ago' +%Y-%m-%d)   # macOS: date -u -v-7d +%Y-%m-%d
curl https://api.cloudflare.com/client/v4/graphql \
  -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" \
  -d '{"query":"query($zone:String!){viewer{zones(filter:{zoneTag:$zone}){httpRequests1dGroups(limit:7,filter:{date_geq:\"'"$SINCE"'\"}){sum{requests cachedRequests}}}}}","variables":{"zone":"'"$ZONE_ID"'"}}' \
  | jq '[.data.viewer.zones[0].httpRequests1dGroups[].sum]
        | {cached:(map(.cachedRequests)|add), total:(map(.requests)|add)}
        | {cached, total, ratio:(.cached/.total*100)}'

Varnish

# Live stats (refreshed every second)
varnishstat -f MAIN.cache_hit -f MAIN.cache_miss

# One-time snapshot with computed ratio
varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss | awk '
  /cache_hit/  { hit  = $2 }
  /cache_miss/ { miss = $2 }
  END { printf "Hit: %d  Miss: %d  Ratio: %.1f%%\n", hit, miss, hit/(hit+miss)*100 }
'

For time-windowed stats, use varnishlog with vsl-query or set up a Prometheus + Varnish exporter.

Nginx (proxy_cache)

Add these variables to your access log format:

log_format cache_status '$remote_addr - $upstream_cache_status '
                        '"$request" $status $body_bytes_sent';
access_log /var/log/nginx/cache.log cache_status;

$upstream_cache_status returns HIT, MISS, EXPIRED, BYPASS, STALE, or UPDATING.

# Tally cache statuses across the log
awk '{print $3}' /var/log/nginx/cache.log | sort | uniq -c | sort -rn
# Then: HIT / (HIT + MISS + EXPIRED) × 100

WP Rocket / LiteSpeed Cache

You can detect these plugins per response:

  • WP Rocket: no cache header by default — check the end of the HTML source for the footprint comment <!-- This website is like a Rocket, isn't it? Performance optimized by WP Rocket... - Debug: cached@<timestamp> -->
  • LiteSpeed: X-LiteSpeed-Cache: hit

For aggregate stats, you need either server-level logging (Nginx/Apache) or a plugin like Query Monitor that shows cache status per request.

Google Search Console (for SEO impact)

Search Console doesn't show cache hit ratios, but it shows the consequences of a low one. In Core Web Vitals:

  • A high TTFB (Time to First Byte) for crawled pages usually indicates cold cache hits
  • Pages with poor LCP scores often correlate with uncached origin responses

Cross-reference your cache flush events (deploys, plugin updates) with dips in Core Web Vitals scores in Search Console to see the SEO impact of cold cache periods.

What numbers to aim for

Hit ratio Assessment
> 90% Healthy: most visitors get cached responses
70–90% Acceptable: room for improvement, especially for high-traffic pages
50–70% Low: investigate TTL settings and purge frequency
< 50% Critical: significant performance and cost impact

Common causes of low hit ratio

1. Too-short TTL: If your Cache-Control: max-age is 60 seconds, pages expire before they're warmed by organic traffic. Extend TTL to 3600–86400 seconds for mostly-static content.

2. Bypass rules that are too broad: Cache-Control: no-cache or no-store on pages that don't need it. Check if your CMS or framework adds these headers by default.

3. Excessive purging: WooCommerce stores that purge on every stock change can flush their cache hundreds of times per day. Throttle or debounce purge events.

4. Query string variations: example.com/page?ref=newsletter and example.com/page?ref=twitter are treated as separate URLs by most caches. Normalize query strings in your cache configuration to avoid cache fragmentation.

5. No warming: Even a perfect cache configuration produces a low hit ratio immediately after a flush. Without warming, hit ratio recovers slowly as organic traffic refills the cache. With warming, it recovers in minutes.

Building a cache health dashboard

For production sites, track hit ratio over time rather than spot-checking:

  • Cloudflare Analytics (built-in)
  • Varnish + Prometheus exporter + Grafana (self-hosted)
  • CacheBoost run history (shows warming completion, which correlates with hit ratio recovery)

Set an alert when hit ratio drops below 70%: this usually indicates an unplanned cache flush or a configuration issue that needs investigation.

This article is also available in Français.