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
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
stale? methods. Here is an
example using the
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