Use Sensible Long-Lived Cache headers

As some of you might expect, I watch all of my network traffic when I browse the web—you never know when you’ll see something interesting. This afternoon, for example, my curiosity was piqued when I noted that as I browsed around the Zune website, my browser issued conditional HTTP requests to revalidate some resources.

Microsoft sites are getting better and better at following best-practices for performance, including setting long-lived caching headers, so I was surprised to see a conditional request for this file. My first guess was that perhaps the site was sending a Vary header, which can cause unnecessary revalidations.

I was curious to find out what caching directives were sent on the original response, so I re-issued the Request unconditionally (use Fiddler’s context menu, or hit the U key) to avoid getting back the HTTP/304. I found that the original response didn’t include Vary and did include a Cache-Control header:

GET /xweb/lx/xap/ HTTP/1.1

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Length: 1054539
Date: Mon, 25 Jan 2010 22:05:10 GMT
Content-Type: application/x-zip-compressed
ETag: “0a0d701a83ca1:0”
Cache-Control: max-age=4294880896
Last-Modified: Tue, 22 Dec 2009 15:18:24 GMT

Interesting. I flipped over to Fiddler’s “Caching” inspector and saw a note that the max-age header was “malformed” – this is incorrect, but it did immediately give me an idea about what was going on.

RFC2616 specifies that the max-age directive specifies a value in delta-seconds, which is defined as:

3.3.2 Delta Seconds    
Some HTTP header fields allow a time value to be specified as an integer number of seconds, represented in decimal, after the time that the message was received.

        delta-seconds  = 1*DIGIT

Notably, this definition says nothing about the size of the integer, nor does any of the verbiage around the max-age directive in general. However, the description of the Age header gives us a clue which suggests that the original authors of the spec expected a baseline of a 32bit integer:

Age values are non-negative decimal integers, representing time in seconds. If a cache receives a value larger than the largest positive integer it can represent, or if any of its age calculations overflows, it MUST transmit an Age header with a value of 2147483648 (2^31). An HTTP/1.1 server that includes a cache MUST include an Age header field in every response generated from its own cache. Caches SHOULD use an arithmetic type of at least 31 bits of range.

As you’ve likely guessed by now, Internet Explorer (or more specifically, WinINET, the HTTP stack below Internet Explorer) does not properly interpret the max-age=4294880896 value because it is greater than 2^31, the maximum value of a signed 32-bit integer. It thus treats this response as stale, despite the server’s intention to suggest that this resource will remain valid for 136 years. I also found that Opera 10.1 and Safari 4.0.3 make the same mistake, while Firefox and Chrome do not.

I’ve fixed the next version of Fiddler’s Caching inspector to treat values up to 2^63 as valid max-age values, and added a warning for any value over 2^31—sixty-eight years ought to be enough for anybody. 🙂

Now, off to find a contact in the Zune team…

7/14/2010 Update: Improved in IE9; IE9 will accept any value up to 2^63 for the max-age value.


Comments (5)

  1. Setting reasonable values makes sense, but I just discovered I didn’t set mine far ahead enough in my book examples. High Performance Web Sites, Rule 3: Add an Expires Header, p. 22:

    Expires: Thu, 15 Apr 2010 20:00:00 GMT

    This is a far future Expires header, telling the browser that this response won’t be stale until April 15, 2010.

  2. EricLaw [MSFT] says:

    @Steve: That’s one advantage of using the max-age directive: so long as it’s positive (and less than MaxInt, I guess 🙂 then you’ll never need to update your book. (Also, apparently the RFC suggests never sending an Expires header more than 1 year into the future.)

    Your book’s point that Expires is nice because it is supported by HTTP/1.0 clients is valid, of course, although I suspect "pure" 1.0 clients that don’t support max-age are probably pretty rare. And, of course, if you want to send GZIP content, a common practice is to send a past-Expires header and a future-Max-Age directive to prevent caching by 1.0 clients that don’t support compression.

    Either way, the fact that High Performance Web Sites still remains in broad use years after its publication is a great sign!

  3. Steve Clay says:

    Good to know. Do you know of similar limits on (unixtime representations of) dates in Expires and Set-Cookie (AKA the "2038 problem")? Anything over a year seems overkill.

  4. Richard says:

    Looking through the source code, ASP.NET won’t let you set a max-age greater than one year (31536000 seconds):

    public void SetMaxAge(TimeSpan delta){
       if (delta < TimeSpan.Zero)   {
           throw new ArgumentOutOfRangeException(“delta”);
       if (s_oneYear < delta)   {
           delta = s_oneYear;
       if (!this._isMaxAgeSet || (delta < this._maxAge))   {
           this._maxAge = delta;
           this._isMaxAgeSet = true;

  5. Rob Parsons says:

    Was that a full day trip to the Zune Building… Good luck… Why not raise an issue ticket on connect.