Caching in Rails - How ETags and fresh_when work

February 25, 2016   

To make web pages load faster, caching is super important. Let's first look at what caching means:

Caching means to store content generated during the request-response cycle and to reuse it when responding to similar requests (from Rails Guides)

Let's break down what happens when a request is sent to a server to figure out what this all means:

First Request

  1. A request is sent to server
  2. Renders entire response body
  3. Generates an ETag by doing an MD% hash on entire response body (looks like below)
  4. headers['ETag'] = Digest::MD5.hexdigest(body)
  5. Rails sends back the response body as well as the ETag as a header field to the client (usually a browser)
  6. The client caches the response and stores the ETag

Second Request

  1. We request the same page again
  2. The client sends the stored ETag as a If-None-Matchheader
  3. Renders entire reponse body
  4. Generates an ETag in the same way as previous
  5. Compares ETag that was sent as header to the one that was just generated
  6. If it matched, then the body is not sent to the client and instead sent a 304 not modified status

Basically if the page isn't modified, the browser reads the page from the cache.

So what are ETags?

ETags stands for Entity Tags. It's essentially a key to see if the page has changed or not.

From a client side perspective, this is awesome because now we don't have to re-download everything, everything is stored in the cache.

But from a server side perspective, it still has to generate the ETag every single time there is a request. Processing the entire response body everytime we want to generate an ETag is not the most efficient.

Introducing fresh_when

You can set your custom ETags with fresh_when. For example, let's say you have a blog application and you want to tell rails to update the cache when an article has been updated or created. You can simply add fresh_when in your controller action. In this case, we're going to add it in the show action.

def show
  @article = Article.find(params[:id])

What this does is generate an ETag for us like this:

headers[‘Etag’] = Digest::MD5.hexdigest(@article.cache_key)

The cache_key is what keeps track of if something has updated. For instance, @article.cache_key would look something like 'article/23-2016-224150000'.

The cache_key is generated in this format: <model name>/<id>-<updated_at>.

According to a simple test that thoughtbot ran, performance improved by 94% (33ms to 2ms) just by including one fresh_when in a controller action. That's an amazing improvement.

As we can see fresh_when is a powerful tool that we can use to improve our caching. Instead of processing the whole response body, we can focus on tracking if a resource is stale or fresh by using fresh_when.

Happy coding!