Basic Rails API Caching

Rails performance out of the box is acceptable for prototypes and small applications with limited traffic. However as
your application grows in popularity you will inevitably be faced with the decision to either add more servers, or use
your existing servers more efficiently. Complex caching strategies can be incredibly difficult to implement correctly,
but simple caching layers can go a long way.

In this post I’ll explain two basic Rails caching mechanisms and explain some of the costs and benefits of each.

HTTP Caching #

If your API responses are mostly static content like a list of available products, then
HTTP Caching can be a very effective solution. Even something
as low as a one minute cache can move the vast majority of your requests to a CDN like
CloudFlare or an in-memory store like
rack cache.

Specifying the expiration time is simple. In your controller action just call expires_in with the time:

class ProductsController < ApplicationController
  def index
    expires_in 1.minute, public: true
    @products = Product.all
  end
end

This will result in a header being set to Cache-Control: max-age=60, public which any CDN will pick up and serve for
you instead of the request hitting your server.

This solution works well when the content is mostly static but it comes with the downside that changes to your content
will not be seen for up to one minute (or whichever time you have chosen).

Conditional GET #

Another option is using ETags or Last-Modified times to know what version of the resource the client has last seen and
returning a HTTP 304 Not Modified response with no content if the resource has not changed.

To set this up in a controller you can either use the
fresh_when or
stale? methods. Here is an
example using the fresh_when method.

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when(etag: @product, last_modified: @product.created_at, public: true)
  end
end

This method attaches an ETag header and a Last-Modified header to every product response. Now if you make a request
for a given product you will see the headers in the response:

curl -i localhost:3000/products/1.json
HTTP/1.1 200 OK
ETag: "91206795ac4c5cd1b02d8fcbc752b97a"
Last-Modified: Mon, 27 May 2014 09:00:00 GMT
...

And if you make the same request but include the ETag in a If-None-Match header, the server can return 304 with empty
content and save all the time it would have spent rendering the content.

curl -i localhost:3000/products/1.json \
  --header 'If-None-Match: "91206795ac4c5cd1b02d8fcbc752b97a"'
HTTP/1.1 304 Not Modified
Etag: "91206795ac4c5cd1b02d8fcbc752b97a"
Last-Modified: Mon, 27 May 2014 09:00:00 GMT
...

The other option is to use the If-Modified-Since header in the request, which will have the same result:

curl -i localhost:3000/products/1.json \
  --header 'If-Modified-Since: Mon, 27 May 2014 09:00:00 GMT'
HTTP/1.1 304 Not Modified
Etag: "91206795ac4c5cd1b02d8fcbc752b97a"
Last-Modified: Mon, 27 May 2014 09:00:00 GMT
...

This method still requires a request to be made to the Rails app, and the product still has to be pulled from the
database to determine the created_at time. However, rendering the response body can be a substantial portion of
each server response so this is a simple way to save a lot of time.

These examples are only the beginning of the caching options Rails offers. As of Rails 4
page caching as well as
action caching have been pulled out into their own gems and are
worth looking at if you need those options.

Finally if you are ready for something a little more robust you can read about Basecamp’s
Russian Doll Caching to see how they solve
these problems.

 
15
Kudos
 
15
Kudos

Now read this

How to keep your data consistent with foreign key constraints

We all have that co-worker (and have been that co-worker) who SSHs into a server and runs SQL statements against live data. On staging servers this can be a minor issue if things go wrong, but in production it can be disastrous. At... Continue →