Lyte's Blog

Bad code, bad humour and bad hair.

X-Cache and X-Cache-Lookup Headers

I watched a discussion about X-Cache and X-Cache-Lookup headers unfold recently and it turns out a lot of people who I would have thought knew what these headers were indicating were a little muddled up. Further more, it turned out if you go looking for a good explanation, everyone seems to just link to this rather old blog post – despite being well meaning, it’s unfortunately slightly confused too.

So maybe I can give a better explanation.

First question – where is the spec for X-Cache? Well, there isn’t one – that little X- prefix indicates the header is not part of the spec, so its meaning may vary between proxy implementations (in fact with Varnish it’s common to add it yourself in the config file, so it could mean anything at all).

Why am I seeing these headers?

So with that out of the way I’ll focus on where I think you’re most likely to see them, Squid. Squid is a fairly common caching proxy, both as a forward (outbound) caching proxy and as a reverse (inbound/application accelerator/aggregating) proxy. Squid’s doco also fails to clearly define what the headers do, so if you’ve got here because you’re trying to figure out what they mean, there’s a good chance you’ve got a Squid proxy in the mix (even if you didn’t realise it).

What they mean

So, with Squid what these headers mean is:

  • X-Cache: Did this proxy serve the result from cache (HIT for yes, MISS for no).
  • X-Cache-Lookup: Did the proxy have a cacheable response to the request (HIT for yes, MISS for no).

This means if:

  • both are HITs, you made a cacheable request and the proxy had a cacheable response that matched and it handed it back to you.
  • X-Cache is a MISS and X-Cache-Lookup is a HIT, you made a request that had a cacheable response but you’ve forced a cache bypass – usually achieved by a hard refresh, commonly activated with Ctrl+F5 or by sending the headers Pragma: no-cache (HTTP/1.0) or Cache-Control: no-cache (HTTP/1.1).
  • both are MISSes, you made a request (it doesn’t matter if it was cacheable) and there was no corresponding response object in the proxy cache.

This is completely irrelevant to browser cache – if you’re viewing browser cache and inspecting the headers in Chrome or Firebug then you’ll see what the status of the proxy was at the time the proxy returned it to your browser. Sorry if this is obvious, but a surprising number of people seemed to think that the browser cares and bothers to modify these headers, it doesn’t. Really, it doesn’t. I promise.

How you can test this for yourself

First, if you’re trying to use a browser to inspect headers, understand what it’s really saying, e.g in Google Chrome, look for the (from cache) next to the Status Code: section in the headers:

This means nothing has gone over the network and it brought the object out of browser cache.

All other browsers are left as an exercise for the reader as frankly it’s not the right tool for the job (I just figured I had to cover one, as everyone seems to do it this way anyway).

Fire up a bash terminal and get used to curl.

This is what I usually run:

1
2
3
4
$ curl -sv http://example.com/ 2>&1 > /dev/null | egrep '< (X-Cache|Age)'
< Age: 1
< X-Cache: HIT from example.com
< X-Cache-Lookup: HIT from example.com:80

There’s a lot going on here, so lets break it down:

  • curl: this is the main thing we’re running,
  • -sv: this enables both --silent and --verbose, which (counterintuitive I know) is the simplest way to get curl in to a mode where it dumps both the body and headers out in a useful format (yes I know you can use --head, but that changes the request format and invalidates enough testing to make it useless).
  • http://example.com/: our URL of choice.
  • 2>&1: Take STDERR (headers) and redirect it to STDOUT (so we can pipe it to egrep).
  • > /dev/null: Ignore the original STDOUT (body).
  • | egrep: pipe the headers through egrep.
  • '< (X-Cache|Age): grab just headers starting with X-Cache or Age.

You may note I snuck that Age header you’ve been ignoring in there, you can thank me later.

So in this first example we’ve made a cacheable request (absent of Pragma: no-cache or similar headers) and the proxy has had a response from its upstream 1 second ago (Age: 1) that was cacheable.

Now that we have a basic command, lets fiddle with it to see what happens.

If we request the same thing, we should see the Age header count up by roughly the number of seconds since the start of either request:

1
2
3
4
$ curl -sv http://example.com/ 2>&1 > /dev/null | egrep '< (X-Cache|Age)'
< Age: 5
< X-Cache: HIT from example.com
< X-Cache-Lookup: HIT from example.com:80

If we request the same thing but with a hard refresh header (note: Squid and probably every other caching proxy ever can be configured to ignore these headers on the request side, so if you get different results here, 9 times out of 10 that’s why), we can see that the proxy had the object in cache, but didn’t use it:

1
2
3
$ curl -sv --header "Pragma: no-cache" http://example.com/ 2>&1 > /dev/null | egrep '< (X-Cache|Age)'
< X-Cache: MISS from example.com
< X-Cache-Lookup: HIT from example.com:80

If we request something we don’t expect the proxy to have seen before, the proxy neither has it in cache or serves it from cache:

1
2
3
$ curl -sv http://example.com/?"$(date -Ins)" 2>&1 > /dev/null | egrep '< (X-Cache|Age)'
< X-Cache: MISS from example.com
< X-Cache-Lookup: MISS from example.com:80

Comments