Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

What Is a Cache Miss, and Why Your Hit Ratio Is Lower Than You Think

Sean

Platform Writer

Sep 01, 2026
8 min read

A cache miss is a lookup that finds nothing, forcing the system to fetch from the slower source behind it. Some misses are unavoidable and some are configuration mistakes, and telling them apart is the whole job.

What Is a Cache Miss, and Why Your Hit Ratio Is Lower Than You Think

Every cache has misses. A cache with a 100% hit ratio is either serving one thing or lying to you. What matters is which kind of miss you are getting, because three of the four kinds are fixable and the fixes are usually one header.

The most common finding when someone actually investigates: the cache is not full, the content has not changed, and the miss is happening because the cache key is different every time for reasons nobody intended.

Table of contents

The four kinds of miss

Classifying the miss tells you what to do about it.

Compulsory, or cold. The first request for an item. Nothing was cached because nothing had been asked for yet. Unavoidable by definition, and only a problem when it happens more often than it should, which is what the other three categories describe.

Capacity. The cache is full and evicted this item to make room. The fix is more space or fewer things cached, and it is diagnosed by an eviction rate that is high relative to your write rate.

Invalidation or expiry. The item was there and its lifetime elapsed, or something purged it. Fixed by longer time-to-live values where correctness allows, and by purging precisely rather than broadly.

Key fragmentation. The item is cached, but under a different key than the one being looked up. This is not a standard textbook category and it is by far the most common cause of a disappointing hit ratio in practice, so it gets its own section.

Diagnostically: high evictions means capacity. Low evictions with low hit ratio means keys or expiry. That single split narrows the investigation immediately.

Key fragmentation, the one that actually gets you

A cache key is whatever the cache uses to decide two requests are for the same thing. On an HTTP cache that is typically the method, the host, the full path including the query string, and anything named in the Vary header.

Which means all of these are separate cache entries for one image:

/hero.jpg
/hero.jpg?utm_source=newsletter
/hero.jpg?utm_source=twitter&utm_campaign=launch
/hero.jpg?v=1699887766
/hero.jpg?fbclid=IwAR2xK9...

Analytics and click-tracking parameters are appended by other people’s platforms, not by you, and each unique value creates a fresh entry that will be a miss for every visitor. On a site with heavy social sharing this alone can halve a hit ratio.

The fix is to configure the cache to ignore query parameters that do not change the response, or to normalise them away. Most CDNs support either an allow list of parameters that matter or a deny list of ones that do not.

The other major fragmenter is the Vary header. Vary: Accept-Encoding is fine, because there are a handful of encodings. Vary: User-Agent is a disaster, because there are effectively unlimited user agent strings and you have just told the cache to store a separate copy for each browser build in existence.

# Look at what you are actually varying on.
curl -sI https://example.com/hero.jpg | grep -i -E 'vary|cache-control|age'

Cookies, and the silent refusal to cache

The second most common cause of an unexpectedly low hit ratio is that the cache is declining to store the response at all, and nothing tells you.

Most shared caches will not store a response carrying a Set-Cookie header, because a cookie is by definition specific to one visitor and serving it to another would be a serious bug.

The trap is that many frameworks start a session on every request, including anonymous ones, and issue a session cookie whether or not anything was stored in it. Every response then carries Set-Cookie and nothing caches, on a site with no logged-in users at all.

# If this returns anything on an anonymous page, that page is not being cached.
curl -sI https://example.com/ | grep -i set-cookie

The fix is in the application: do not start a session until something needs one. For a static asset path it is usually enough to ensure the framework is not in the request path at all.

The related mistake is the missing Cache-Control header. With no directive, caches apply conservative defaults or refuse to store, and a great many sites are serving cacheable assets with no caching instruction whatsoever.

Reading whether a request hit or missed

Nearly every cache reports its decision in a response header, and learning to read yours before you need it at 2am is worth ten minutes.

curl -sI https://example.com/hero.jpg | grep -i -E 'cf-cache-status|x-cache|age|cache-control'

The values you will see, whatever the header is called:

  • HIT: served from cache. What you want.
  • MISS: not in cache, fetched from origin, and now stored.
  • EXPIRED: it was there, its lifetime elapsed, and it was revalidated.
  • BYPASS or DYNAMIC: a rule or a header told the cache not to store this at all. If you see this on something that should cache, that is your bug.
  • REVALIDATED or STALE: served while checking with the origin, which is the stale-while-revalidate behaviour working.

The Age header is also informative: it tells you how many seconds the cached copy has existed. An Age that resets constantly on a busy asset means something is evicting or invalidating it more often than you intended.

The headers that produce a good hit ratio

Two patterns cover most content, and the difference between them is whether the URL changes when the content does.

# Build assets with a content hash in the filename.
# The URL changes when the content does, so cache forever.
Cache-Control: public, max-age=31536000, immutable

# HTML and anything at a stable URL that can change.
Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=86400

# Genuinely per-user responses.
Cache-Control: private, no-store

The middle one is the workhorse and deserves reading carefully. max-age=0 tells browsers to revalidate every time. s-maxage=600 tells shared caches to hold it for ten minutes. stale-while-revalidate=86400 says that if the origin is slow or down, serve the stale copy for up to a day while fetching a fresh one in the background.

That last directive is quietly one of the best availability features available in a header. It converts an origin outage from an error page into slightly stale content, which most visitors will not notice.

The response headers documentation covers setting these on RunxBuild static sites, and getting them right is usually a bigger win than any other caching change.

How this fits the rest of the stack

A high hit ratio is one of the few changes that improves speed and cost at the same time, since every cached response is bandwidth and compute your origin never spends. Worth knowing what those cost before optimising: the RunxBuild hosting calculator shows plan size alongside bandwidth, with 120GB included on static sites and $0.10/GB beyond, so the value of a ten-point hit ratio improvement becomes a number.

Useful related references:

FAQ

What is the difference between a cache hit and a cache miss?

A hit means the requested item was found in the cache and served from there. A miss means it was not found, so the system fetched it from the slower source behind the cache and usually stored a copy for next time. Hits are fast and cheap; misses cost a round trip to the origin.

What is a good cache hit ratio?

For a static-heavy site, above 90% is healthy. Between 50% and 80% usually indicates missing cache-control headers, cookies on anonymous responses, or query parameters fragmenting the cache key. Below 50% means the cache is functioning as a proxy rather than a cache.

Why is my content not being cached at all?

The two usual causes are a Set-Cookie header on the response, which most shared caches refuse to store, and no Cache-Control header, which makes caches apply conservative defaults. Check both with a single curl of the response headers before investigating anything more complex.

Do query strings affect caching?

Substantially. By default the full URL including the query string is part of the cache key, so tracking parameters appended by social platforms and email tools create a separate entry per unique value. Configure the cache to ignore parameters that do not change the response.

What does stale-while-revalidate do?

It permits a cache to serve an expired copy while fetching a fresh one in the background. The visitor gets an immediate response and the cache updates behind them. It also means an origin outage degrades to slightly stale content rather than an error page, which makes it one of the most valuable directives available.

#what is a cache miss#cache hit ratio#cdn caching#cache key#http caching