X-Cache

Caching

X-Cache is a non-standard response header a cache adds to report how it handled a request: HIT means it answered from a stored copy, MISS means it forwarded toward origin. The name, the token set and the token order differ by vendor. RFC 9211 Cache-Status is the standardised replacement.

10 min read Updated Aug 30, 2026

Full Explanation

X-Cache is a non-standard HTTP response header. A caching intermediary adds it to a response to report how it handled the request. HIT means the cache answered from a stored copy. MISS means it could not, and forwarded the request toward the origin. It is a debugging convention rather than part of HTTP. No HTTP specification defines it. It is absent from IANA's HTTP Field Name Registry, where Cache-Status and Age are both registered. RFC 6648 advises creators of new parameters not to prefix their names with an X- or similar construct at all.

It is also not a directive and not a metric. Nothing you send in X-Cache changes caching behaviour. Cache-Control and the cache key decide that. A single response is one observation from one cache server, not a cache hit ratio. The header name, the token vocabulary, and even the order of the tokens are per-vendor: CloudFront sends Hit from cloudfront, Fastly sends HIT or MISS, Akamai's debug output uses TCP_HIT, Cloudflare reports the same signal in CF-Cache-Status, and Varnish emits X-Varnish instead. That divergence is exactly why RFC 9211 defines Cache-Status with standardised syntax and semantics. It is also why Squid 6 stopped emitting X-Cache in favour of it.

How it works

The header is written by the cache, on the way back to the client. Clients never send it. Origins do not normally set it. Instead, an edge server, CDN node, or reverse proxy appends it to the response it just served. Fastly, for example, "appends this non-standard header to all responses by default". It derives the value from its internal state variable. On a HIT, the cache serves what it already holds. The origin sees no request at all. On a MISS, the request is forwarded. The response is usually stored. The next request for the same object, from the same cache node, can be a HIT.

When a response crosses more than one cache tier, the header carries one token per cache that handled it. You have to know the vendor's order to read it. Fastly lists tokens in the order the servers processed the response. So with shielding, the shield POP comes first and the edge POP that answered you comes last. MISS, HIT means the edge had the object. That leading MISS is a record of a past fetch from the shield, not the state of the shield now. Fastly is explicit that where the second entry is HIT, "the first entry in each of the three debugging headers relates to when the object was originally fetched from the shield, not the current status of the object at the shield". Akamai's equivalent runs the other way. Its Return Cache Status behaviour reports Miss from child, Hit from parent, with edge tier first.

X-Cache answers "which cache served this", not "how old is it". Freshness is reported separately by the Age header. RFC 9111 defines Age as the sender's estimate of the time since the response was generated or successfully validated at the origin server, not the time since this cache stored it. Age is only present on responses that came from cache, so on a MISS you will usually see no Age at all.

Why it matters for a CDN

It is the cheapest check that the edge, and not your origin, answered the request. Read across a page's requests, it also tells you which problem you have. A page of nothing but MISS points at cacheability, not at the CDN. The cause could be Cache-Control or Expires from the origin, or an eligibility rule that excludes the content. HIT and MISS mixed across URLs that should behave the same points at the cache key instead. Query strings, cookies, and forwarded request headers each split one URL into many keys. AWS notes that if "two requests to access an object have different query string values, then the second request isn't served from the cache". Repeated MISS on a hot object points at eviction, or at fragmentation across nodes (see cache miss types). This happens because a CDN caches per node. CloudFront "caches the object only in the edge location that received the request". A fresh request routed to another POP is therefore a MISS by design.

It is also the header that makes multi-tier accounting visible. On Fastly, HIT, MISS is a miss at the edge served from the shield. It still "contribute[s] one 'miss' to your headline CHR although ultimately the request is satisfied from within the Fastly network". Your hit ratio and your origin offload are therefore not the same number. The header is where you can see the difference.

What CDNs do

  • AWS CloudFront sends X-Cache as a result word followed by the provider name. In practice this is Hit from cloudfront, Miss from cloudfront, or Error from cloudfront. The same result vocabulary, including RefreshHit for an expired object revalidated at the origin, is what CloudFront documents for its x-edge-response-result-type log field. Responses also carry x-amz-cf-pop, identifying the edge location that answered. X-Cache is on CloudFront's list of headers you cannot remove with a response headers policy, so you cannot suppress it that way.
  • Fastly appends X-Cache and a companion X-Cache-Hits by default. It derives X-Cache from fastly_info.state. A PASS is reported as MISS, while serving stale, background revalidation, and edge-generated synthetic responses all report as HIT. The header carries several tokens when shielding or Next-Gen WAF at Edge is enabled, or when custom VCL calls restart, so two or three tokens are common in practice. You can unset it in vcl_deliver, or gate it behind the Fastly-Debug request header.
  • Cloudflare does not document X-Cache. Instead it reports the outcome in CF-Cache-Status, whose documented values are HIT, MISS, NONE/UNKNOWN, EXPIRED, STALE, BYPASS, REVALIDATED, UPDATING, and DYNAMIC. The two negative verdicts differ in timing. DYNAMIC means Cloudflare decided at request time that the asset was not eligible for cache. BYPASS means it was eligible, but "the origin response was ultimately not cacheable". UPDATING is the expected status while an asynchronous stale-while-revalidate refresh is in flight.
  • Akamai returns X-Cache with TCP_ values: TCP_HIT, TCP_MISS, TCP_MEM_HIT, TCP_REFRESH_HIT, TCP_NEGATIVE_HIT, and others. These appear only as debug output, requested with the legacy akamai-x-cache-on Pragma header or the modern Akamai-Debug header, which is gated behind an auth token. For production traffic, Akamai's Return Cache Status behaviour writes Hit, Miss, RefreshHit, HitStale, or NotCacheable into a header you name yourself.
  • Varnish emits no X-Cache of its own. It adds X-Varnish, carrying the current transaction ID, plus the ID of the transaction that populated the cache when the request was a hit. So two space-separated IDs mean a hit, and one means a miss. An X-Cache header on a Varnish deployment is something the operator added in VCL.
  • nginx exposes the outcome as the variable $upstream_cache_status, with values MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED, and HIT. The value reaches the client only if the operator publishes it with add_header, conventionally as X-Cache-Status. That is a third header name for the same idea.
  • Squid has moved on. Squid 6 implements RFC 9211 Cache-Status. That header "replaces X-Cache and X-Cache-Lookup which are no longer emitted by Squid".

Watch out for

  • The vocabulary is not portable. No specification defines these tokens. So HIT in one vendor's header does not mean what HIT means in another's. Tools that parse them need per-vendor rules. Token order is not portable either: Fastly puts the edge last, Akamai puts it first.
  • MISS does not mean uncacheable. The first request for a perfectly cacheable object is a MISS. On Fastly a PASS also reports as MISS. On Cloudflare the not-cached verdicts are DYNAMIC (not eligible at request time) and BYPASS (eligible, but the origin response was not cacheable), not MISS.
  • A token can describe the past. In Fastly's two-token output, the shield entry reflects when the object was originally fetched. This means MISS, HIT does not mean the shield is missing this object right now.
  • One response is one node. Because a CDN stores per node and per POP, a HIT and a MISS for the same URL minutes apart is normal. X-Cache-Hits is worse. Fastly's count is "per-cache-server, not per-data-center". Because clustering spreads objects unevenly, Fastly itself calls the value "of very limited use in most cases". Do not read popularity out of it.
  • Publishing it has a cost. RFC 9211 warns that cache status lets attackers probe the cache, because "knowing if a cache has stored a response can help an attacker execute a timing attack on sensitive data". One documented incident shows the practical version. An attacker DoSing an application with random query strings, a cache-busting pattern, watched X-Cache and stopped as soon as the operator's VCL fix flipped the responses from MISS to HIT. Note that the cache-poisoning risk in RFC 9211 comes from exposing the cache key, which plain X-Cache does not do. Akamai's debug family does expose it, through X-Cache-Key and X-True-Cache-Key.

Best practice

  • Read the cache-status header together with Age and the vendor's served-by header (X-Served-By, x-amz-cf-pop), not on its own. Repeated curl -I requests on the URL you actually care about tell you far more than a single response.
  • Request twice before concluding anything, and from more than one location. A first MISS followed by a HIT means the system is working. Two MISSes from the same node is a finding.
  • Triage in this order. All MISS means look at Cache-Control and cache eligibility. Mixed HIT and MISS across equivalent URLs means audit the cache key, including query strings, cookies and the Vary header. On Cloudflare, with Origin Cache Control enabled (the default on Free, Pro and Business plans), a request carrying Authorization is cacheable only if Cache-Control also includes public, s-maxage or must-revalidate.
  • Do not compute a hit ratio from headers. Do not read origin offload out of a headline hit ratio when a shield tier is in play. The vendor counts shield hits differently from edge hits.
  • Prefer RFC 9211 Cache-Status wherever a cache in your path emits it, because its parameters are defined rather than guessed. Where you cannot, keep the vendor header for your own debugging, and gate or strip it on public traffic if the platform lets you. Fastly can unset it in vcl_deliver, while CloudFront's X-Cache cannot be removed with a response headers policy.

Examples

# Check cache status with curl
$ curl -sI https://cdn.example.com/style.css | grep -i 'x-cache\|cf-cache\|age'
X-Cache: HIT
Age: 3247

# Different CDN headers:
# Cloudflare: CF-Cache-Status: HIT
# CloudFront: X-Cache: Hit from cloudfront
# Fastly: X-Cache: HIT, X-Cache-Hits: 42
# Akamai: X-Cache: TCP_HIT from a23.51.224.170
# Varnish: X-Varnish: 12345 67890 (two IDs = HIT)

# Common values:
# HIT - served from cache
# MISS - fetched from origin, now cached
# EXPIRED - TTL expired, revalidating
# BYPASS - not cached by rule
# DYNAMIC - uncacheable content

Frequently Asked Questions

X-Cache is a non-standard response header a cache adds to report how it handled a request: HIT means it answered from a stored copy, MISS means it forwarded toward origin. The name, the token set and the token order differ by vendor. RFC 9211 Cache-Status is the standardised replacement.

# Check cache status with curl
$ curl -sI https://cdn.example.com/style.css | grep -i 'x-cache\|cf-cache\|age'
X-Cache: HIT
Age: 3247

# Different CDN headers:
# Cloudflare: CF-Cache-Status: HIT
# CloudFront: X-Cache: Hit from cloudfront
# Fastly: X-Cache: HIT, X-Cache-Hits: 42
# Akamai: X-Cache: TCP_HIT from a23.51.224.170
# Varnish: X-Varnish: 12345 67890 (two IDs = HIT)

# Common values:
# HIT - served from cache
# MISS - fetched from origin, now cached
# EXPIRED - TTL expired, revalidating
# BYPASS - not cached by rule
# DYNAMIC - uncacheable content

Related CDN concepts include:

  • Age Header — The Age response header is a cache's estimate, in seconds, of how long ago the …
  • Vary Header — Vary is the response header that names the request headers the origin used to select …
  • Cache-Control — Cache-Control is the HTTP header field, defined in RFC 9111, that carries caching directives to …
  • Cache Hit Ratio (CHR) — The share of requests a cache answers from its own stored copies instead of fetching …
  • TTL (Time To Live) (TTL) — TTL (time to live) is how many seconds a cached response stays fresh before a …