Measuring your real cache hit rate

Philipp Philipp 9 min read
Illustration of a cache hit rate chart rising after caching is enabled, with visitor requests on the left and a breakdown into hit 78 percent, miss 12 percent, bypass 7 percent and stale 3 percent on the right

Here is how I judged caching plugins before I started digging into caching properly: install the plugin, open the homepage, notice that it snaps, run one PageSpeed test, done. Fast site.

What those tests measured was a handful of pages, each freshly warmed by the test before it, requested as an anonymous visitor from a fast connection, on one afternoon. In other words, the cache’s best case. Real traffic is thousands of requests across every URL a site has, from crawlers and shoppers and logged-in users, right after content edits and expiries, and it never stops. How many of those requests get the fast path is a number, and I had no idea what mine was.

That number is the cache hit rate, and it is the most honest metric a page cache has. It is also the number I would rather MilliCache be judged on than any speed test: I think it comes out ahead of every other WordPress cache on exactly this measure, and the point of this post is that you can check that yourself. It is about measuring the hit rate, reading it, and catching the configuration mistakes that quietly ruin it while every spot check still feels fast.

Why this one number matters#

A cached response and an uncached one have almost nothing in common. On our test site, MilliCache Pro’s own measurement puts the cached response at 17 ms and the uncached one at 75 ms, and that is a small site on fast hardware. On the production network in the screenshot further down, a well-tuned one, the same two numbers are 12.9 ms and 407 ms; on a typical shop with a heavy theme and a full set of plugins, the uncached render runs to well over a second. Your site’s real-world speed is the weighted average of the two, and the weight is the hit rate:

  • 95% hit rate, at 15 ms and 800 ms: 0.95 × 15 + 0.05 × 800 ≈ 54 ms on average
  • 80% hit rate, same numbers: 0.80 × 15 + 0.20 × 800 ≈ 172 ms on average

Same server, same plugin, same snappy homepage: three times the difference in what an average visitor experiences, and the slow tail is worse than the average. Every point of hit rate you lose is a slice of your traffic getting the slow site.

Speed tests cannot see this, because the page you test becomes warm by being tested. The misses happen elsewhere: on the archive page a crawler opens at 4 a.m., on the product page behind a campaign link, on everything you just cleared. Field data, the Core Web Vitals numbers Google actually ranks you on, is collected from real visitors, so it includes your misses even when your tests never do.

What quietly eats your hit rate#

The frustrating thing about every item on this list: pages still load, spot checks still pass, nothing errors. The hit rate is the only place they show up.

  • Cookie lists that are too broad. One wildcard too wide on the No-Cache list, and a whole class of visitors bypasses the cache for good.
  • Full flushes. Every “clear everything” sets the whole site back to cold. If clears are frequent, the cache never finishes warming, and the chart shows a sawtooth.
  • Short lifetimes without grace. Entries die faster than traffic can rewarm them.
  • Campaign parameters you did not tell the cache about. MilliCache ignores utm_* out of the box, but fbclid, gclid or a newsletter tool’s own tag mint a new cache entry per click unless you add them to the ignored request keys.
  • Bypass rules wider than intended. A path pattern that matches more than you thought, and a chunk of the site is simply never cached.

Measuring it for free#

MilliCache shows its hit ratio out of the box: the Status tab of the settings screen has it next to the entry count and cache size, with the last seven days of requests split into cached and uncached. That is your baseline, no setup required.

To go beyond spot checks, sample your own sitemap and count the X-MilliCache-Status header, which every response carries:

terminal
 1# Use a leaf sitemap; the index (wp-sitemap.xml) only lists other sitemaps
 2curl -s https://example.com/wp-sitemap-posts-post-1.xml 
 3  | grep -oE 'https://[^<]+' | head -50 
 4  | while read -r url; do
 5      curl -s -o /dev/null -D - "$url" 
 6        | awk -F': ' 'tolower($1)=="x-millicache-status" { print tolower($2) }'
 7    done | sort | uniq -c

Run it twice. The first pass warms whatever was cold; the second pass tells you what the cache looks like to a visitor arriving after normal traffic. On our test site:

the same six URLs, sampled twice
 1pass 1:   2 hit
 2          4 miss
 3pass 2:   6 hit

A healthy content site’s second pass is nearly all hit. A stale here and there is fine: an expired page was served instantly while a fresh one was built, which is grace doing its job. Lots of bypass? Switch on debug mode and read the X-MilliCache-Reason header it adds; a rule is wider than you think. Still lots of miss on the second pass? Something is removing entries between your two runs, and that something is worth finding. And use a full GET as the script does, not curl -I: HEAD requests are cached separately, so a HEAD sample reports a cache your visitors never touch.

For questions about what is in the cache, the CLI answers per content type. On a network the size of the one in the screenshot below, the answers look like this:

terminal
 1$ wp millicache stats
 2entries     1340
 3size        39.23 MB
 4
 5$ wp millicache stats --flag="post:*"
 6entries     612
 7size        21.4 MB
 8
 9$ wp millicache stats --flag="archive:*"
10entries     297
11size        9.8 MB

That tells you what is in the cache. Combined with the header sampling, which tells you what is served from it, that is the whole free diagnosis. On a multisite network, flags carry the site ID, so write 1:post:* for site 1.

What a good number looks like#

There is no universal target, but there are honest reference points. A content site with mostly anonymous traffic should live above 90 percent, often well above. A WooCommerce shop runs lower by design, because carts and checkouts bypass; what matters there is that the catalog pages hit. Sites with many logged-in users (memberships, communities) are lower still, and the interesting number becomes the bypass share: is everyone bypassing who should, and nobody who should not?

Which is the general rule: the goal is not 100 percent. A bypass on a checkout is the cache working correctly. The goal is that every miss and every bypass has a reason you could name.

Watching it over time#

A single reading is a snapshot; the failures listed above are all patterns over time. The sawtooth after every publish. The cliff after a deploy. The slow slide as a campaign mints URL variants. You cannot see any of that in one number; you need the chart.

That chart is what MilliCache Pro’s Detailed Metrics module adds. The Status tab becomes a dashboard: requests, hit ratio and bandwidth over time, hourly or daily, for the last 7, 14 or 30 days or any date range you pick, each figure with its change against the period before. Three details make it useful for diagnosis rather than decoration:

MilliCache Pro Detailed Metrics dashboard for a multisite network: 1,340 entries, 93.4 percent hit ratio, 57.3k requests over three days, cached responses at 12.9 ms against 407 ms uncached
Detailed Metrics on a real Multisite network, all sites, last three days: 93.4% served from the cache, and a cache hit 32 times faster than a render.
  • Response time is measured, not assumed. The dashboard charts cached against uncached delivery from your real traffic, with the “~N× faster” summary and a “visitor time saved” figure. That last one is the number to put in front of a client or a boss: caching translated into human waiting time avoided.
  • Stale serves are counted separately, so you can see grace doing its job: how often visitors got the instant, slightly old page instead of waiting for a render.
  • Recording never gets in the way of serving. Metrics are written after the response has gone out, and a storage hiccup is dropped rather than surfaced to the visitor.

History is kept in hourly detail for two months and in daily totals for two years, both adjustable in the settings. The free plugin keeps seven days of hourly and thirty days of daily data. On a multisite network the network dashboard shows all sites together or any one of them.

A workflow that actually improves the number#

  1. Baseline. Note your hit ratio and run the sitemap sampler. With MilliCache Pro, look at a two-week chart.
  2. Read your bypasses. Every reason string that surprises you is a finding.
  3. Change one thing. Narrow a cookie list, add grace, replace a full-flush habit with flag-targeted clearing, give a variant its own bucket instead of bypassing it.
  4. Watch the shift. With the chart, a config fix is visible within hours: the sawtooth flattens, the bypass share drops, the response-time gap narrows.

If you want to see what the dashboard looks like before real traffic arrives, wp millicache metrics seed fills it with sample data on a staging site, and wp millicache metrics clear starts over with real data only.

You configured your cache once. Your traffic, your content and your plugins change every week. The hit rate is how you notice.

Try it#

MilliCache ships the measuring basics: the hit ratio on the Status tab, the X-MilliCache-Status header on every response, with bypass reasons once debug mode is on, and wp millicache stats with per-flag filtering. Everything in the free section above works today.

MilliCache Pro turns measurement into monitoring: Detailed Metrics charts hit ratio, requests, bandwidth and measured cached-versus-uncached response times over months of history, counts stale serves and shows visitor time saved, while the Cache Entries browser lets you inspect exactly what is cached, with which flags, when a number looks off.

Get new articles first

Guides, updates and release notes in your inbox. No spam.

By subscribing you agree to our Privacy Policy.­ Double opt-in, no spam, unsubscribe anytime.

Leave a comment