Features
xSpeed Hub Pricing Docs Blog Scan
Appearance
Get Plugin

Understanding cache HITs and MISSes

Getting Started 7 min

Every request is either a HIT or a MISS. Read the X-XSpeed-Cache header, learn which layer served the hit, and find out why some pages always miss.

Every request to your site is either a HIT — answered from a saved copy of the page — or a MISS, one WordPress had to build from scratch. Knowing which is which, and which layer served the hit, is the fastest way to confirm caching is working.

You don’t have to guess. xSpeed stamps its verdict on every front-end response, so a single command tells you what happened. And the first thing to know is that if you check while signed in to WordPress, the answer will never be a hit.


The X-XSpeed-Cache header

xSpeed adds this header to every front-end response. Its value answers two questions at once: was this cached, and what served it.

ValueMeaningTypical speed
HIT (nginx)Served as a static file by nginx, before PHP runs at all.~5–15 ms
HIT (static)Served as a static file by Apache via .htaccess, before PHP runs.~25–40 ms
HIT (php)Served from cache by xSpeed’s PHP drop-in. WordPress booted, but nothing was rendered or queried.~85 ms
MISSNot cached. WordPress rendered it fresh — and stored the result, so the next request should hit.Full page load
BYPASSxSpeed decided this request must never be cached.Full page load

HIT (nginx) is the fast path you want. All three HITs are real caching, but the gap between the top row and HIT (php) is roughly an order of magnitude, because the top two skip PHP entirely. Which one your site can reach depends on your server — What is xSpeed Cache? covers the three paths and how to get to the fastest one available to you.

A MISS is only a problem if it repeats. The first request for any page after a purge is a miss by definition; that request is what creates the cached copy. Reload and you should see a hit. If the same URL misses every single time, that is a real finding — and Why some pages always MISS below usually explains it.


How to check it

From the command line, with no browser state to confuse things:

curl -s -o /dev/null -D - https://example.com/ | grep -i x-xspeed-cache

Run it twice. The first call may report MISS; the second should report a hit. To see every header xSpeed sets, including the bypass reason when WP_DEBUG is on:

curl -s -o /dev/null -D - https://example.com/ | grep -i '^x-xspeed'

⚠️ Don’t use curl -I. It sends a HEAD request, and only GET requests are served from cache — so curl -I reports BYPASS every time, even on a perfectly healthy site. The commands above send a real GET and simply throw the page body away.

In the browser, open your site in a private window, open developer tools and select the Network tab, then reload and click the first request — the HTML document, not an image or a script. The value is under Response Headers.

⚠️ Your own logged-in visits are never cached, and never will be. logged-in is the second gate xSpeed checks, above almost everything else. A cached page is by definition a page that’s identical for everyone, and an admin bar, a personal greeting and a form nonce are none of those things.

So testing your cache while signed in reports BYPASS on every request, forever, on a perfectly healthy site. This is the single most common reason someone concludes caching isn’t working. Test in a private window, or with curl.

If the header is missing altogether, that is itself a finding. Either page caching is off — check the Caching switch in the top bar — or something upstream answered before your server did, in which case you’ll usually see a CDN’s own cache header beside it. If neither applies, the drop-in isn’t loading: wp-content/advanced-cache.php is missing, or WP_CACHE isn’t defined in wp-config.php. Health checks both and tells you which.


Why some pages always MISS

Some requests are deliberately never cached. When the value is BYPASS and WP_DEBUG is enabled, a second header — X-XSpeed-Reason — names the gate that stopped it. The slug is fixed per gate: it never echoes the URL, cookie or user-agent that matched, so the header is safe to read and safe to share.

  • logged-in — somebody is signed in. Expected, and not fixable by design.
  • excluded-url — the path matched one of your Excluded URLs.
  • excluded-cookie — a cookie matching your Excluded Cookies was present.
  • query-param — the URL carried a query parameter that isn’t in the ignored list.
  • non-get — a POST or other non-GET request: a form submission, a comment, a checkout — and the HEAD request curl -I sends.
  • cache-disabled — page caching is off entirely.
  • non-frontend — an admin, AJAX, cron or REST request. Nothing to cache.
  • donotcachepage — another plugin or your theme set the DONOTCACHEPAGE constant for this request.
  • user-agent — the visitor’s user-agent matched one of your Bypass User Agents.
  • post-excluded — this individual post has caching disabled in its own cache settings.
  • filtered — code on your site answered the xspeed_cache_skip_for_post filter.

Three of those account for most surprises.

query-param is the one people hit first. Any parameter outside the ignored list makes the URL a distinct request, so xSpeed declines rather than file it under the clean URL and risk serving the wrong page. Analytics parameters — utm_*, fbclid, gclid and the rest of a 31-entry default list — are ignored by default precisely so campaign traffic still hits the cache. If your site uses its own parameter that doesn’t change the page, add it to Ignored Query Parameters in Page Cache.

excluded-url is usually working exactly as intended. The defaults exclude /cart, /checkout, /my-account, feeds, the REST API and the login screen — the pages where a cached copy would be actively wrong. Finding one of those bypassed is a pass, not a failure.

excluded-cookie covers the logged-out-but-stateful visitor. WooCommerce and Easy Digital Downloads cart cookies are on the default list, so a shopper with something in their basket gets a freshly built page even though they never signed in.


The hit ratio

Hit ratio on Overview and Health is hits ÷ (hits + misses) over a rolling 24 hours, kept in hourly buckets. A healthy public site trends high — 80%+ on a site with steady repeat traffic.

Two things are deliberately kept out of that figure, and both make it more honest rather than more flattering. Bot and scanner misses don’t count: a wave of requests for /wp-x7.php from a vulnerability scanner is noise, not a measurement of how your cache serves visitors, so those are recorded as a separate absorbed-requests line instead of cratering the denominator, and 404s are treated the same way. And bypassed requests aren’t misses — a bypass was never a candidate for the cache, so counting it would make a correctly configured WooCommerce site look broken.

A low ratio on a site that otherwise looks healthy usually means the counting, not the caching. The likeliest cause is an edge in front of your origin: if a CDN or Cloudflare is answering requests at the edge, they never reach your server to be counted, so your visitors are getting the faster result and your ratio simply can’t see it. Failing that, a large site with long-tail traffic misses a lot because each page is requested rarely — the preloader crawls your sitemap so visitors meet a warm cache instead of creating it. And if you only enabled caching an hour ago, the window is 24 hours; you have an hour of data.