The HTTP protocol is, paradoxically enough, one of the most misunderstood protocols on the internet. A very expressive and feature-rich protocol, most applications make use of only a small subset of the functionality it provides. In so doing, they miss out on powerful scalability features, built right into the very infrastructure of the web.
The REST architecture movement, however, is increasing awareness of these things. Nowadays the trend is towards using these completely free infrastructural resources in the way they were intended. By so doing, complicated and expensive proprietary middleware can be eliminated as it is no longer needed to provide scalability and robustness to enterprise-size applications.
HTTP caching is one of the most important and powerful features of the HTTP protocol. It's heavily standardised, it's remarkably sophisticated, it's available everywhere on the web, and it doesn't cost a penny.
The Web is Full of Caches
A typical user, when connecting to a web site, has the impression that they are connected directly to the target site:
In reality, the connection might look more like this:
The above still leaves out many intermediary networking devices and even some devices that may also serve as caches in places where you don't expect it (such as the local NAT device in your home, which sometimes doubles as a cache). But you get the idea: caches are everywhere on the web.
All these devices cooperate according to very stringent specifications. They are guaranteed to behave in a predictable manner. Many applications ignore their existence, never taking advantage of the caching facilities they offer: to such applications and their clients, the caches will be invisible. However, it's very easy to leverage their power and control them even if you don't own or operate them, or even know how many intermediate caches there are. The HTTP standard specifies how this is done.
Ocean provides fine-grained control of HTTP caching through the means described in the following sections.
The Cache-Control HTTP Header
There are various HTTP headers to instruct caches how to cache, or not to cache, requests. If no such information is available in the request, caches will effectively treat the request in a way that is equivalent to not caching it at all.
HTTP 1.1 added the
Cache-Control HTTP header. Its purpose is to provide explicit control of cache behaviour. Most developers are aware that the
Cache-Control can be set in HTTP responses. However, it can also be very useful when sending requests.
When used in a response, the following
Cache-Control header would allow only a browser cache to store the entity, and even then it must check back with the origin server each access to determine whether the entity is fresh. If it is, the browser cache uses its own copy of the entity (thereby saving bandwidth), otherwise a new entity is requested from the origin server:
Cache-Control: private, max-age=0, must-revalidate
Cache-Control header, when included in an HTTP response, would instruct all caches to store and serve the entity for an hour. During that period, they will not revalidate any subsequent requests with the origin server, no matter how many. This is a frequently used idiom when serving static assets, such as images, for a public website.
Cache-Control: public, max-age=3600
Cache-Control header which will let a response be cached by all caches (don't let the
no-cache fool you into thinking otherwise), but require each subsequent request to validate data with the origin server. In this way we are using intermediary caches to distribute data, while still making it possible to authorise every access for separate users, something which is essential in an API. The origin server is hit every time, but substantial savings are done in bandwidth as the vast majority of replies will be
304 Not Modified responses which lack a body.
Cache-Control: public, no-cache, must-revalidate
Here's an example which when included in an HTTP request from a user agent such as a browser effectively says, "Give me a result which is guaranteed to be valid for at least 5 more minutes, or if none exists, request the origin server for a new one":
As you can see, the
Cache-Control attributes may be combined to produce very sophisticated results.
Here are some of the most useful
- Permits intermediary shared caches, that is, caches that serve more than one consumer, to cache the entity. The main use case is for caching non-sensitive, public data such as images, CSS, and JS.
- Indicates that all or part of the response message is intended for a single user and must not be cached by a shared cache. A private (non-shared) cache, such as a browser cache, may cache the response.
- Specifies that the entity may be cached for a given number of seconds, during which it may be served without revalidating with the origin server.
- This is similar to
max-age, but it only applies to shared caches. This attribute is used extensively in Ocean to restrict caching to the Varnish layer only. Whenever
s-maxageappears in a response from the Ocean servers, Varnish removes the
s-maxageattribute from the
Cache-Controlheader in the response and adds a new
- Caches are allowed to serve stale representations under special conditions. By specifying this attribute, you’re telling caches that they may not serve stale entities but must revalidate them with the origin server first. This attribute does not specify that every request must be revalidated, only those for stale entities. For instance, the effect of a
max-ageattribute is still the same during the specified period of freshness. Neither does
must-revalidaterequire the revalidated entity to be transferred once again: to conserve bandwidth, the entity may be served from the local cache.
- This is used to control how caches should handle staleness for this entity. It specifies that the entity may still be served after it has become stale for a given number of seconds. One use case has to do with robustness: to allow caches to serve non-critical data even if the origin server is unreachable. Stale responses served by an HTTP cache will always include a
Warning: 110 (Response is stale)HTTP header.
- Indicates that the client is willing to accept a response that will still be fresh for at least the specified number of seconds.
- This attribute does not prohibit an entity from being stored in a cache; rather, it tells the cache that it must revalidate the freshness of the entity each time it is requested. To save bandwidth, the cache is still permitted to store a copy of the entity and serve it if the revalidation succeeds. Caveats: IE6 might misbehave and respond from its cache anyway, and later versions of IE and Firefox have started to treat
no-store, probably due to the confusion that has arisen from the fact that
no-cacheisn't intended to prevent caches from storing the entity.
- This instructs caches not to keep a copy of the representation under any conditions. The purpose of this directive is to prevent the inadvertent release or retention of sensitive information. A typical use case is to prevent data from accidentally appearing in backup media. The cache must only hold the information during the request-response cycle, after which it must be destroyed.
For a complete explanation of all valid attributes, c.f. http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.
HTTPS adds the restriction that shared caches must never cache any copies of the encrypted entity, so a connection using HTTPS will only bring a local private cache such as a browser cache into action. Intermediary shared caches do not enter the picture.
This behaviour can be overridden using the
Requests containing the
Authentication HTTP header will never be cached.
This behaviour can be overridden using the
Freshness and Revalidation
Central to the concept of HTTP caching are two concepts:
- When an entity is deemed fresh the cache may serve it at once, without consulting the origin server first. A header such as
Cache-Control: max-age=3600instructs caches to treat the entity as fresh for a period of an hour. During that hour, the origin server will never be consulted for revalidation; the cache will simply serve the entity without delay. Normally, the server has no way of expiring entities in caches, whether remote or local, shared or private. (In Ocean, however, we do have full control over one shared cache: Varnish.)
- When an entity no longer is deemed fresh but has become stale, the cache may still serve up a stored entity after revalidating it. The cache consults its upstream source (which may be another cache) to see if the entity has changed. If it's unchanged and still valid, the cache will regard the locally stored entity as fresh once more and will serve it from the local cache; in order to save bandwidth, an unchanged entity will not be downloaded from the origin again. Only when the entity has changed will a fresh copy of the entity be downloaded.
There's a variety of HTTP headers which provide the necessary information for determining whether an entity has changed or not. The most important are
Last-Modifiedheader is used in responses: its value is a GMT timestamp representing the last time the entity was modified. The
Last-Modifiedheader typically is used with single entities, such as a JSON resource or an image, which have some kind
updated_atattribute or a file modification date. It typically does not appear in headers for entities representing collections of resources.
- During revalidation, the
Last-Modifiedtimestamp of the origin entity is compared to the last known local timestamp given by the
If-Modified-Sinceheader of the revalidation request. If there is a match, the local entity is still valid. If not, the local entity is stale.
ETagheader is also used in responses. Its value is an unique, opaque identifier representing a specific version of the entity. More often than not the value is an MD5 hash of the contents or the last update time of the entity, which guarantees that as soon as an entity changes, its
ETagwill also change.
ETagheaders are very useful with collections: typically the
ETagfor a collection is composed of the composite MD5 of the data type of the collection, the number of members in the collection, and the modification date of the last member in the collection to be updated. This allows the ETag to be calculated using only two DB accesses, no matter the size of the collection. ETags should be surrounded by double quotes.
- During revalidation, the
ETagvalue of the origin entity is compared to the last known
ETaggiven by the
If-None-Matchheader. If there is a match, the local entity is still valid. If not, the local entity is stale.
Expiresheader properly belongs to the HTTP 1.0 epoch. Its value is an absolute time in GMT, which can raise problems of synchronisation. As a
Cache-Controlheader supersedes any
Expiresheader, we recommend you to use the
- This header also belongs to the HTTP 1.0 epoch. Use
- This isn't really an HTTP header but an HTML tag appearing in the request payload. Don't expect intermediary caches to parse the contents of your request payload. They won't, and besides, the payload is encrypted if you're using HTTPS. This directive will have no effect except on a local browser, and even then the results are dicey at best. Use a proper
Cache-ControlHTTP header instead.
Cacheable entities should include one or both of the
ETag headers. When a client receives such an entity, it should store the values of those headers for later requests. The next time the entity is requested, regardless of whether it is fresh or stale, the client should pass the values of those two headers back as the values of the
If-None-Match headers, respectively:
|Received in a response...||... re-used in subsequent requests|
Last-Modified: Thu, 17 Nov 2011 12:08:07 GMT ETag: "09d8b2988932a19258941fad7e254d47"
If-Modified-Since: Thu, 17 Nov 2011 12:08:07 GMT If-None-Match: "09d8b2988932a19258941fad7e254d47"
This is all that is needed to fully support HTTP caching and Conditional
- If the header values don't match, a new representation of the entity will be transferred (which will include new
ETagheaders to be stored and re-used in the next request). The status will be a 2xx.
- Please note that
If-None-Matchvalues both must be surrounded by double quotes. If they're not, they won't match, the Conditional GET won't work and you'll receive a 2xx rather than a 304.
- If the header values do match, the response will be an HTTP
304 Not Modified. The 304 has no payload: it only consists of headers. Its purpose is to tell the client that its locally cached entity is still valid and can be re-used, thus saving time and bandwidth.
Caching in Ocean
An informal article explaining the considerations in a very understandable way:
The RFC spec:
A discussion of how browsers handle the
An overview of HTTP caching in Ruby on Rails:
The Varnish Book: