HTTP Caching

From Ocean Framework Documentation Wiki
Jump to: navigation, search

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:

StraightConnection.png

In reality, the connection might look more like this:

RealisticConnection.png

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

The following 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

Here's another 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":

Cache-Control: min-fresh=300

As you can see, the Cache-Control attributes may be combined to produce very sophisticated results.

Cache-Control Attributes

Here are some of the most useful Cache-Control attributes:

public
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.
private
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.
max-age=
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.
s-maxage=
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-maxage appears in a response from the Ocean servers, Varnish removes the s-maxage attribute from the Cache-Control header in the response and adds a new Age: 0 header.
must-revalidate
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-age attribute is still the same during the specified period of freshness. Neither does must-revalidate require the revalidated entity to be transferred once again: to conserve bandwidth, the entity may be served from the local cache.
max-stale=
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.
min-fresh=
Indicates that the client is willing to accept a response that will still be fresh for at least the specified number of seconds.
no-cache
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-cache like no-store, probably due to the confusion that has arisen from the fact that no-cache isn't intended to prevent caches from storing the entity.
no-store
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

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.

HTTPSConnection.png

This behaviour can be overridden using the Cache-Control directive.


Authenticated Requests

Requests containing the Authentication HTTP header will never be cached.

AuthenticationConnection.png

This behaviour can be overridden using the Cache-Control directive.

Bottle.jpg NOTE: The Authentication header isn't used by Ocean's authentication and authorisation system.


Freshness and Revalidation

Central to the concept of HTTP caching are two concepts:

Freshness
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=3600 instructs 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.)
Revalidation
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.

Validators

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-Modified and ETag.

Use these

Last-Modified
The Last-Modified header is used in responses: its value is a GMT timestamp representing the last time the entity was modified. The Last-Modified header typically is used with single entities, such as a JSON resource or an image, which have some kind updated_at attribute or a file modification date. It typically does not appear in headers for entities representing collections of resources.
During revalidation, the Last-Modified timestamp of the origin entity is compared to the last known local timestamp given by the If-Modified-Since header of the revalidation request. If there is a match, the local entity is still valid. If not, the local entity is stale.
ETag
The ETag header 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 ETag will also change. ETag headers are very useful with collections: typically the ETag for 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 ETag value of the origin entity is compared to the last known ETag given by the If-None-Match header. If there is a match, the local entity is still valid. If not, the local entity is stale.

Avoid these

Expires
The Expires header 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-Control header supersedes any Expires header, we recommend you to use the max-age attribute instead.
Pragma
This header also belongs to the HTTP 1.0 epoch. Use Cache-Control instead.
<meta http-equiv="Cache-Control">
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-Control HTTP header instead.

Conditional GETs

Cacheable entities should include one or both of the Last-Modified and 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-Modified-Since and 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 GETs.

  • If the header values don't match, a new representation of the entity will be transferred (which will include new Last-Modified and ETag headers to be stored and re-used in the next request). The status will be a 2xx.
  • Please note that ETag and If-None-Match values 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

Caching in Ocean


Links

An informal article explaining the considerations in a very understandable way:

The RFC spec:

A discussion of how browsers handle the no-store directive:

An overview of HTTP caching in Ruby on Rails:

The Varnish Book: