Client HTTP Caching in Rails

Of all the available caching methods, client caching is both one of the most powerful, and most overlooked. I admit, I'm guilty of this myself! So what is client HTTP caching? I'm referring to the conditional If-Modified-Since HTTP requests issued by the browser/spider (Section 14.25) The idea is simple, when you submit a request to the server, you stuff a date into the 'If-Modified-Since' header and the web-server decides how to serve the request based on this timestamp. If the date you provided is equal to the last-modified date on the server, then a 304 header (Not Modified) is returned, and otherwise you browser starts fetching the actual file.

Now, this is not a blanket caching method, but these requests are often used by RSS readers (check if the feed has been updated), and it is also a great way to cache dynamically generated objects (xml sitemaps, dynamic charts, etc.) Bottom line is, it's virtually free, and it can dramatically lessen the load on the server - don't overlook it!

Serving conditional requests

As with most things in Rails, HTTP caching is extremely easy to implement, so let's jump to the code right away:

 def widget
    @widget = Widget.find(@params['id'])

    # Check if this is a conditional request
    cacheTime = Time.rfc2822(@request.env["HTTP_IF_MODIFIED_SINCE"]) rescue nil

    # if conditional request and cacheTime is equals or less than created_at timestamp.. use cache!
    if cacheTime and @widget.created_at <= cacheTime
      return render :nothing => true, :status => 304

    # Either non-conditional request, or timestamp is outdated, serve the request
      # Set the Last-Modified header so the client can cache the timestamp (used for later conditional requests)
      @response.headers['Last-Modified'] = @widget.created_at.httpdate

      # .... Your code to 'generate' the widget/RSS feedi/etc.

      # render ...

You can certainly extract some of these conditionals into private helper methods, but for the sake of an example let's just step through some of the code. All conditional requests carry the 'If-Modified-Since' timestamp for which we probe the incoming headers. If such a timestamp is found, we parse it and store it for later use. Next, we check if the received timestamp is in fact up to date. If the timestamp matches the local object on the server, we simply return a '304 Not Modified' back the client - the client will use a local copy and our web-server doesn't have to do a thing! Otherwise, we continue serving the request but with one caveat, if you want the item to be cached on the client, you have to set the 'Last-Modifed' header prior to returning the request.

Who said there is no free lunch?

After adding a few of these conditionals I saw a drastic decrease in RSS fetch requests on some of my sites! The clients are happy because their software runs faster, and I'm happy because I don't have to waste my bandwidth or CPU to serve redundant requests. It can't get much better than that!

Ilya GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.