This blog is part of our Rails 5 series.

Rails has powerful tools to control caching of resources via HTTP such as fresh_when and stale?.

Previously we could only pass a single record to these methods but now Rails 5 adds support for accepting a collection of records as well. For example,

def index
  @posts = Post.all
  fresh_when(etag: @posts, last_modified: @posts.maximum(:updated_at))
end

or simply written as,

def index
  @posts = Post.all
  fresh_when(@posts)
end

This works with stale? method too, we can pass a collection of records to it. For example,

def index
  @posts = Post.all

  if stale?(@posts)
    render json: @posts
  end
end

To see this in action, let’s begin by making a request at /posts.

$ curl -I http://localhost:3000/posts

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/"a2b68b7a7f8c67f1b88848651a86f5f5"
Content-Type: text/html; charset=utf-8
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 7c8457e7-9d26-4646-afdf-5eb44711fa7b
X-Runtime: 0.074238

In the second request, we would send the ETag in If-None-Match header to check if the data has changed.

$ curl -I -H 'If-None-Match: W/"a2b68b7a7f8c67f1b88848651a86f5f5"' http://localhost:3000/posts

HTTP/1.1 304 Not Modified
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
ETag: W/"a2b68b7a7f8c67f1b88848651a86f5f5"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 6367b2a5-ecc9-4671-8a79-34222dc50e7f
X-Runtime: 0.003756

Since there’s no change, the server returned HTTP/1.1 304 Not Modified. If these requests were made from a browser, it would automatically use the version in its cache on the second request.

The second request was obviously faster as the server was able to save the time of fetching data and rendering it. This can be seen in Rails log,

Started GET "/posts" for ::1 at 2016-08-06 00:39:44 +0530
Processing by PostsController#index as HTML
   (0.2ms)  SELECT MAX("posts"."updated_at") FROM "posts"
   (0.1ms)  SELECT COUNT(*) AS "size", MAX("posts"."updated_at") AS timestamp FROM "posts"
  Rendering posts/index.html.erb within layouts/application
  Post Load (0.2ms)  SELECT "posts".* FROM "posts"
  Rendered posts/index.html.erb within layouts/application (2.0ms)
Completed 200 OK in 31ms (Views: 27.1ms | ActiveRecord: 0.5ms)


Started GET "/posts" for ::1 at 2016-08-06 00:39:46 +0530
Processing by PostsController#index as HTML
   (0.2ms)  SELECT MAX("posts"."updated_at") FROM "posts"
   (0.1ms)  SELECT COUNT(*) AS "size", MAX("posts"."updated_at") AS timestamp FROM "posts"
Completed 304 Not Modified in 2ms (ActiveRecord: 0.3ms)

Cache expires when collection of records is updated. For example, an addition of a new record to the collection or a change in any of the records (which changes updated_at) would change the ETag.

Now that Rails 5 supports collection of records in fresh_when and stale?, we have an improved system to cache resources and make our applications faster. This is more helpful when we have controller actions with time consuming data processing logic.