Episodic Genius


occurring occasionally and at irregular intervals


Understanding HTTP caching

I’ve been thinking about caching strategy lately. Specifically in the context of HTTP 1.1 and web-based applications. It is relevant both at work and even more in my project I’m working on at home. Here are some thoughts I’ve had about it.

It all boils down understanding freshness and validity without confusing the two. A common misconception is to confuse stale with invalid. In fact, the only relationship between freshness and validity is that you can assume that a fresh response is valid. They don’t correlate any other way.


The following is a table of some useful combinations of cache-control directives and validation mechanisms. There are more combinations possible. These are some of the ones that I’ve used. The column headings at the top are validation strategies. These are implemented on the server by examining “If-Modified-Since” (Date Validation) or “If-None-Match” (ETag validation) headers sent by the client. Fingerprint validation is a special case.

The row header column on the left contains various Cache-Control directives that can be sent by the server with a response. Again, it is not exhaustive.


NoneDateETagFingerprint
no-storeMust Not Cache---
no-cache-Must Validate DateMust Validate ETag-
max-age: 0-Should Always Validate DateShould Always Validate ETag-
max-age: nFinite WindowShould Validate Date if StaleShould Validate ETag if Stale-
max-age: ∞Immutable--URL Fingerprinting

Note that ∞ is not actually a valid max-age according to the HTTP 1.1 specification. In fact, it says not to use anything longer than a year. There is no good reason for this but I always follow the recommendation of the specification anyway in case there are caches out there that do not allow a value greater. In practice, when I want an infinite freshness lifetime I simply use 31536000 for max-age. That is the number of seconds in 365 days.

Always Validate

If a resource is likely to be valid in a cache but it could change at any time then you should consider the “always validate” strategy. You still pay the cost of a round-trip to the server but you do not have to pay the cost of transferring the entity’s contents if they are valid. This can still be a significant advantage.

Besides saving the time and network resource to transfer the contents, this strategy can have a more subtle advantage. Serving the entire contents may require the server to go to disk or even another network resource somewhere else. This can be expensive. Often, positive validation of an entity can be accomplished using information found in memory on the server. It might be possible to perform the validation extremely quickly before loading any external resource. This can dramatically reduce the load on the server and results in a very low-latency response.

Of course, if the resource is not likely to be valid in the cache then this is all just waste of precious server time. You’ll want to weigh the pros and cons in the context of your situation.

See the section on the Client Control directive for another important point regarding the must-revalidate directive.

Finite Freshness Window

I’m not a fan of freshness windows other than 0 and ∞. Either a resource can change or it can’t. If it can, it is often hard to say how long it is guaranteed to be valid. I usually see some sort of compromise between a window that is too short for effective caching and too long so that it prevents changing the resource in a timely manner.

The advantage is easy development. If you decide that implementing a validation mechanism on the server is just not worth the effort, you can tolerate a period of time where some caches serve an old resource after you update it and you can tolerate serving the resource to a cache that already had it but couldn’t validate it then this mechanism may be for you. If you are building an application that needs to scale and still perform, please consider a stronger caching mechanism.

ETag vs Date

I always prefer ETag over Date validation. I think it is easier to think about whether the resource is different or not rather than when it changed. This is related to why I don’t like the finite freshness window. I really find it awkward to think about time durations and specific dates when considering the validity of an entity. Time durations and dates have nothing to do with it.

With that said, you might still find Date validation useful in certain situations. I’ve used it, mostly when I didn’t get to design the system myself and it wasn’t designed with ETag validation in mind. When you use date based validation, make sure that you properly set the Last-Modified header in the original response. Otherwise, many clients will not attempt date based validation. I’ve
made this mistake myself once and seen others make it as well.

As an aside, I find most software build systems awkward for the same reason. Most of them use time stamps and up-to-date checks. I’ve been burned by them a number of times.

Development vs Deployment

During development, your resources are changing all the time. Many developers turn off caching altogether during development. When you release and deploy your application, many resources stop changing for a long period of time. I don’t like turning off caching during development because testing the caching strategy gets pushed off to the end or not done at all. My favorite caching strategies work correctly and optimally during both development and deployment.

URL Fingerprinting

Fingerprint validation is a special case which is not mentioned in the HTTP 1.1 specification but is a very interesting validation mechanism. The idea is to generate a URL that is unique to a specific entity. Effectively, you turn a single mutable resource in to lots of immutable resources with distinct URLs. Another way to think about is embedding the ETag in to the URL instead of the response headers. The rules for the URL now are essentially the same as for a strong ETag, it must be different if the resource changes.

What is interesting about fingerprint validation is that it does not require an extra round-trip to the server. It only works with resources that are referenced from another resource. The validation of the entity is embedded in the referring resource. This is why it works so well with Javascript, CSS, images and other resources referenced from HTML documents.

Note that fingerprint validation requires work on the server side just like any other validation mechanism. In fact, it can be tricky especially if you use it during development where the resource is changing quite often. The server doesn’t do the validation work when the resource is requested. Rather, it must do the validation work when any resource that references it is requested. This is a bit more complicated since it is common for a number of resources to refer to the same resource.

Client Control

The HTTP 1.1 specification has mechanisms for the client to control some aspects of caching. This isn’t often considered by application developers. The reason is that most applications do not have any control over the client because the client is usually whatever browser the end-user happens to be using.

All application developers should, at least, be aware that a client can be configured to accept stale responses. If your application will not work well this way then you should consider using the no-cache, must-revalidate and proxy-revalidate directives which will override a compliant client.

Cache Cleanup

I’ve heard concern with using long freshness lifetimes and bloating caches. In fact, the two are not correlated. Just about every cache has a mechanism for cleaning out old data. In general, they do not consider the freshness of the cache entry at all. The most common strategy is to purge the least recently used entries.

Caveats

max-age: I’ve heard that max-age is not as well supported as the Expires header. I’ve not run in to this because the work that I’ve done with browsers excludes all but a small, specific set of recent browsers. I test with each supported browser individually and have confirmed that max-age works correctly in all of them. That is a luxury that you may not have. If you must support the widest possible set of browsers then you should consider using the Expires header instead of max-age. The idea is the same except that I find it is inconvenient to have to calculate the Expires header from the current time and my desired max-age and then format it properly for the header. The more I can avoid working with date formats in the protocol the better.

You might consider using max-age in your server software and then using an output filter to convert any max-age directives to Expires headers. You’ll have to weigh the pros and cons here for yourself.

no-cache: From reading the HTTP 1.1 specification, the no-cache directive is supposed to act about the same as “max-age=0, must-revalitate”. It seems that in some browser and cache implementations, it may actually behave like the no-store directive.

AJAX: I’ve had trouble with some browsers not caching AJAX requests. This has been annoying to me but I don’t fully understand the problem. I’ll update this post with details as I learn them.