9780596521424
caching_r279568_id35801036.html

Chapter 9. Caching

Guest chapter by Adam Hawkins! This chapter is an adaptation of his classic article on caching in Rails.

Caching Strategies

First, let's start with a brief overview of the different types of caching. We'll start from 50,000ft and work our way down.

  1. HTTP Caching: Uses HTTP headers (Last-Modified, ETag, If-Modified-Since, If-None-Match, Cache-Control) to determine if the browser can use a locally stored version of the response or if it needs to request a fresh copy from the origin server. Rails makes it easy to use HTTP caching, however the cache is managed outside your application. You may have noticed the config.cache_control and Rack::Cache, Rack::ETag, Rack::ConditionalGet middlewares. These are used for HTTP caching.

  2. Page Caching: PRAISE THE GODS if you actually can use page caching in your application. Page caching is the holy grail. Save the entire thing. Don't hit the stack & give some prerendered stuff back. Great for worthless applications without authentication and other highly dynamic aspects. This essentially works like HTTP caching, but the response will always contain the entire page. With page caching the application is skipping the work.

  3. Action Caching: Essentially the same as page caching, except all the before filters are run allowing you to check authentication and other stuff that may have prevented the request form rendering.

  4. Fragment Caching: Store parts of views in the cache. Usually for caching partials or large bits of HTML that are independent from other parts, e.g. a list of top stories.

  5. Rails.cache: All cached content except cached pages are stored in the Rails.cache. We'll use this fact that later. You can cache arbitrary content in the Rails cache. You may cache a large complicated query that you don't want to wait to reinstantiate a ton of ActiveRecord::Base objects.

Under the Hood

All the caching layers are built on top of the next one. Page caching and HTTP caching are different because they do not use Rails.cache The cache is essentially a key-value store. Different things can be persisted. Strings are most common (for HTML fragments). More complicated objects can be persisted as well. Let's go through some examples of manually using the cache to store things. I am using memcached with dalli for all these examples. Dalli is the default memcached driver.

# Rails.cache.write takes two values: key and a value
> Rails.cache.write 'foo', 'bar'
=> true

# We can read an object back
> Rails.cache.read 'foo'
=> "bar"

# We can store a complicated object as well
> hash = { :this => { :is => 'a hash' }}
> Rails.cache.write 'complicated-object', object
> Rails.cache.read 'complicated-object'
=> {:this=>{:is=>"a hash"}}

# If we want something that doesn't exist, we get nil
> Rails.cache.read 'we-havent-cached-this-yet'
=> nil

# "Fetch" is the most common pattern. You give it a key and a block
# to execute to store if the cache misses. The blocks's return value is
# then written to the cache. The block is not executed if there is a
# hit.
> Rails.cache.fetch 'huge-array' do
    huge_array = Array.new
    1000000.times { |i| huge_array << i }
    huge_array # retrun value is stored in cache
  end
=> [huge array] # took some time to generate
> Rails.cache.read 'huge-array'
=> [huge array] # but returned instantly

# You can also delete everything from the cache
> Rails.cache.clear 
=> [true]

Those are the basics of interacting with the Rails cache. The rails cache is a wrapper around whatever functionality is provided by the underlying storage system. Now we are ready to move up a layer.

Understanding Fragment Caching

Fragment caching is taking rendered HTML fragments and storing them in the cache. Rails provides a cache view helper for this. Its most basic form takes no arguments besides a block. Whatever is rendered during the block will be written back to the cache. The basic principle behind fragment caching is that it takes much less time fetch pre-rendered HTML from the cache, then it takes to generate a fresh copy. This is appallingly true. If you haven't noticed, view generation can be very costly. If you have cachable content and are not using fragment caching then you need to implement this right away! Let's say you have generated a basic scaffold for a post:

$ rails g scaffold post title:string content:text author:string

Let's start with the most common use case: caching information specific to one thing, for example, a single post. Here is a show view:

<!-- nothing fancy going on here -->
<p>
  <b>Title:</b>
  <%= @post.title %>
</p>

<p>
  <b>Content:</b>
  <%= @post.content %>
</p>

Let's say we wanted to cache fragment. Simply wrap it in cache and Rails will do it.

<%= cache "post-#{@post.id}" do %>
  <p>
    <b>Title:</b>
    <%= @post.title %>
  </p>

  <p>
    <b>Content:</b>
    <%= @post.content %>
  </p>
<% end %>

The first argument is the key for this fragment. The rendered HTML is stored with this key: views/posts-1. Wait what? Where did that 'views' come from? The cache view helper automatically prepends 'views' to all keys. This is important later. When you first load the page you'll see this in the log:

Exist fragment? views/post-2 (1.6ms)
Write fragment views/post-2 (0.9ms)

You can see the key and the operations. Rails is checking to see if the specific key exists. It will fetch or write it. In this case, it has not been stored so it is written. When you reload the page, you'll see a cache hit:

Exist fragment? views/post-2 (0.6ms)
Read fragment views/post-2 (0.0ms)

There we go. We got HTML from the cache instead of rendering it. Look at the response times for the two requests:

Completed 200 OK in 17ms (Views: 11.6ms | ActiveRecord: 0.1ms)
Completed 200 OK in 16ms (Views: 9.7ms | ActiveRecord: 0.1ms)

Very small differences in this case. 2ms different in view generation. This is a very simple example, but it can make a world of difference in more complicated situations.

You are probably asking the question: "What happens when the post changes?" This is an excellent question! What well if the post changes, the cached content will not be correct. It is up to us to remove stuff from the cache or figure out a way to get new content from the cache. Let's assume that our blog posts now have comments. What happens when a comment is created? How can handle this?

This is a very simple problem. What if we could figure out a solution to this problem: How can we create a cache miss when the associated object changes? We've already demonstrated how we can explicitly set a cache key. What if we made a key that's dependent on the time the object was last updated? We can create a key composed of the record's ID and its updated_at timestamp! This way the cache key will change as the content changes and we will not have to expire things manually. (We'll come back to sweepers later). Let's change our cache key to this:

<% cache "post-#{@post.id}", @post.updated_at.to_i do %>

Now we can see we have a new cache key that's dependent on the object's timestamp. Check out the Rails log:

Exist fragment? views/post-2/1304291241 (0.5ms)
Write fragment views/post-2/1304291241 (0.4ms)

Cool! Now let's make it so creating a comment updates the post's timestamp:

class Comment < ActiveRecord::Base
  belongs_to :post, :touch => true
end

Now all comments will touch the post and change the updated_at timestamp. You can see this in action by touch'ing a post.

Post.find(1).touch

Exist fragment? views/post-2/1304292445 (0.4ms)
Write fragment views/post-2/1304292445 (0.4ms)

This concept is known as: auto expiring cache keys. You create a composite key with the normal key and a time stamp. This will create some memory build up as objects are updated and no longer fresh. Here's an example. You have that fragment. It is cached. Then someone updates the post. You now have two versions of the fragment cached. If there are 10 updates, then there are 10 different versions. Luckily for you, this is not a problem for memcached! Memcached uses a LRU replacement policy. LRU stands for Least Recently Used. That means the key that hasn't been requested in the longest time will be replaced by newer content when needed. For example, assume your cache can only hold 10 posts. The next update will create a new key and hence new content. Version 0 will be deleted and version 11 will be stored in the cache. The total amount of memory is cycled between things that are requested. There are two things to consider in this approach.

  1. You will not be able to ensure that content is kept in the cache as long as possible.

  2. You will never have to worry about expiring things manually as long as timestamps are updated in the model layer.

I've found it is orders of magnitude easier to add a few :touch => true statements to my relationships than it is to maintain sweepers. More on sweepers later. Let’s keep exploring cache keys.

Rails uses auto-expiring cache keys by default. The problem is they are not mentioned at all the documentation or in the guides. There is one very handy method: ActiveRecord::Base.cache_key. This will generate a key like this: posts/2-20110501232725. This is the exact same thing we did ourselves. This method is very important because depending on what type of arguments you pass into the cache method, a different key is generated. For the time being, this code is functionally equal to our previous examples.

<%= cache @post do %>

The cache helper takes different forms for arguments. Here are some examples:

cache 'explicit-key'      # views/explicit-key
cache @post               # views/posts/2-1283479827349
cache [@post, 'sidebar']  # views/posts/2-2348719328478/sidebar
cache [@post, @comment]   # views/posts/2-2384193284878/comments/1-2384971487
cache :hash => :of_things # views/localhost:3000/posts/2?hash_of_things

If an Array is the first arguments, Rails will use cache key expansion to generate a string key. This means calling doing logic on each object then joining each result together with a /. Essentially, if the object responds to cache_key, it will use that, otherwise it will do various things instead. Here's the source for expand_cache_key:

def self.expand_cache_key(key, namespace = nil)
  expanded_cache_key = namespace ? "#{namespace}/" : ""

  prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
  if prefix
    expanded_cache_key << "#{prefix}/"
  end

  expanded_cache_key <<
    if key.respond_to?(:cache_key)
      key.cache_key
    elsif key.is_a?(Array)
      if key.size > 1
        key.collect { |element| expand_cache_key(element) }.to_param
      else
        key.first.to_param
      end
    elsif key
      key.to_param
    end.to_s

  expanded_cache_key
end

This is where all the magic happens. Our simple fragment caching example could easily be converted into an idea like this: The post hasn't changed, so cache the entire result of /posts/1. You can do with this action caching or page caching.

Moving on to Action Caching

Action caching is an around filter for specific controller actions. It is different from page caching since before filters are run and may prevent access to certain pages. For example, you may only want to cache if the user is logged in. If the user is not logged in they should be redirected to the log in page. This is different than page caching. Page caching bypasses the rails stack completely. Most web applications of legitimate complexity cannot use page caching. Action caching is the next logical step for most web applications. Let's break the idea down: If the post hasn't changed, return the entire cached page as the HTTP response, else render the show view, cache it, and return that as the HTTP response. Or in code:

# Note: you cannot run this code! This is just an example of what's
# happening under the covers using concepts we've already covered.
Rails.cache.fetch 'views/localhost:3000/posts/1' do
  @post = Post.find params[:id]
  render :show
end

Declaring action caching is easy. Here's how you can cache the show action:

class PostsController < ApplicationController

  caches_action :show

  def show
    # do stuff
  end
end

Now refresh the page and look at what's been cached.

Started GET "/posts/2" for 127.0.0.1 at 2011-05-01 16:54:43 -0700
  Processing by PostsController#show as HTML
  Parameters: {"id"=>"2"}
Read fragment views/localhost:3000/posts/2 (0.5ms)
Rendered posts/show.html.erb within layouts/application (6.1ms)
Write fragment views/localhost:3000/posts/2 (0.5ms)
Completed 200 OK in 16ms (Views: 8.6ms | ActiveRecord: 0.1ms)

Now that the show action for post #2 is cached, refresh the page and see what happens.

Started GET "/posts/2" for 127.0.0.1 at 2011-05-01 16:55:27 -0700
  Processing by PostsController#show as HTML
  Parameters: {"id"=>"2"}
Read fragment views/localhost:3000/posts/2 (0.6ms)
Completed 200 OK in 1ms

Damn. 16ms vs 1ms. You can see the difference! You can also see Rails reading that cache key. The cache key is generated from the url with action caching. Action Caching is a combination of a before and around filter. The around filter is used to capture the output and the before filter is used to check to see if it's been cached. It works like this:

  • Execute before filter to check to see if cache key exists

    • Key exists? Read from cache and return HTTP Response. This triggers a render and prevents any further code from being executed.

    • No key? Call all controller and view code. Cache output using Rails.cache and return HTTP response.

Now you are probably asking the same question as before: "What do we do when the post changes?" We do the same thing as before: we create a composite key with a string and a time stamp. The question now is, how do we generate a special key using action caching?

Action caching generates a key from the current url. You can pass extra options using the :cache_path option. Whatever is in this value is passed into url_for using the current parameters. Remember in the view cache key examples what happened when we passed in a hash? We get a much different key than before:

views/localhost:3000/posts/2?hash_of_things

Rails generated a URL based key instead of the standard views key. This is because you may different servers. This ensures that each server has it's own cache key. For example, server one does not collide with server two. We could generate our own URL for this resource by doing something like this:

url_for(@post, :tag => @post.updated_at.to_i)

This will generate this URL:

http://localhost:3000/posts/1?tag=234897123978

Notice the ?tag=23481329847. This is a hack that aims to stop browsers from using HTTP caching on specific urls. If the URL has changed (timestamp changes) then the browser knows it must request a fresh copy. Rails 2 used to do this for assets like CSS and JS. Things have changed with the asset pipeline.

Here's an example of generating a proper auto expring key for use with action caching.

caches_action :show, :cache_path => proc { |c|
  # c is the instance of the controller. Since action caching
  # is declared at the class level, we don't have access to instance
  # variables. If cache_path is a proc, it will be evaluated in the
  # the context of the current controller. This is the same idea
  # as validations with the :if and :unless options
  #
  # Remember, what is returned from this block will be passed in as
  # extra parameters to the url_for method.
  post = Post.find c.params[:id]
  { :tag => post.updated_at.to_i }
end

This calls url_for with the parameters already assigned by it through the router and whatever is returned by the block. Now if you refresh the page, you'll have this:

Started GET "/posts/2" for 127.0.0.1 at 2011-05-01 17:11:22 -0700
  Processing by PostsController#show as HTML
  Parameters: {"id"=>"2"}
Read fragment views/localhost:3000/posts/2?tag=1304292445 (0.5ms)
Rendered posts/show.html.erb within layouts/application (1.7ms)
Write fragment views/localhost:3000/posts/2?tag=1304292445 (0.5ms)
Completed 200 OK in 16ms (Views: 4.4ms | ActiveRecord: 0.1ms)

And volia! Now we have an expiring cache key for our post! Let's dig a little deeper. We know the key. Let's look into the cache and see what it actually is! You can see the key from the log. Look it up in the cache.

> Rails.cache.read 'views/localhost:3000/posts/2?tag=1304292445'
=> "<!DOCTYPE html>\n<html>\n<head>....."

It's just a straight HTML string. Easy to use and return as the body. This method works well for singular resources. How can we handle the index action? I've created 10,000 posts. It takes a good amount of time to render that page on my computer. It takes over 10 seconds. The question is, how can we cache this? We could use the most recently updated post for the time stamp. That way, when one post is updated, it will move to the top and create a new cache key. Here is the code without any action caching:

Started GET "/posts" for 127.0.0.1 at 2011-05-01 17:18:11 -0700
  Processing by PostsController#index as HTML
  Post Load (54.1ms)  SELECT "posts".* FROM "posts" ORDER BY updated_at DESC LIMIT 1
Read fragment views/localhost:3000/posts?tag=1304292445 (1.5ms)
Rendered posts/index.html.erb within layouts/application (9532.3ms)
Write fragment views/localhost:3000/posts?tag=1304292445 (36.7ms)
Completed 200 OK in 10088ms (Views: 9535.6ms | ActiveRecord: 276.2ms)

Now with action caching:

Started GET "/posts" for 127.0.0.1 at 2011-05-01 17:20:47 -0700
  Processing by PostsController#index as HTML
Read fragment views/localhost:3000/posts?tag=1304295632 (1.0ms)
Completed 200 OK in 11ms

Here's the code for action caching:

caches_action :index, :cache_path => proc {|c|
  { :tag => Post.maximum('updated_at') }
}

We'll come back to this situation later. This is a better way to do this. Points to the reader if they know the problem.

These are simple examples designed to show you who can create auto expiring keys for different situations. At this point we have not had to expire any thing ourselves! The keys have done it all for us. However, there are some times when you want more precise control over how things exist in the cache. Enter Sweepers.

Sweepers

Sweepers are HTTP request dependent observers. They are loaded into controllers and observe models the same way standard observers do. However there is one very important different. They are only used during HTTP requests. This means if you have things being created outside the context of HTTP requests sweepers will do you no good. For example, say you have a background process running that syncs with an external system. Creating a new model will not make it to any sweeper. So, if you have anything cached. It is up to you to expire it. Everything I've demonstrated so far can be done with sweepers.

Each cache_* method has an opposite expire_* method. Here's the mapping:

  1. caches_page , expire_page

  2. caches_action , expire_action

  3. cache , expire_fragment

Their arguments work the same with using cache key expansion to find a key to read or delete. Depending on the complexity of your application, it may be easy to use sweepers or it may be impossible. It's easy to use sweepers with these examples. We only need to tie into the save event. For example, when a update or delete happens we need to expire the cache for that specific post. When a create, update, or delete happens we need to expire the index action. Here's what the sweeper would look like:

class PostSweeper < ActionController::Caching::Sweeper
  observe Post

  def after_create(post)
    expire_action :index
    expire_action :show, :id => post
    # this is the same as the previous line
    expire_action :controller => :posts, :action => :show, :id => @post.id
  end
end

# then in the controller, load the sweeper
class PostsController < ApplicationController
  cache_sweeper :post_sweeper
end

I will not go into much depth on sweepers because they are the only thing covered in the rails caching guide. The work, but I feel they are clumsy for complex applications. Let's say you have comments for posts. What do you do when a comment is created for a post? Well, you have to either create a comment sweeper or load the post sweeper into the comments controller. You can do either. However, depending on the complexity of your model layer, it may quickly become infeasible to do cache expiration with sweepers. For example, let say you have a Customer. A customer has 15 different types of associated things. Do you want to put the sweeper into 15 different controllers? You can, but you may forget to at some point.

The real problem with sweepers is that they cannot be used once your application works outside of HTTP requests. They can also be clumsy. I personally feel it's much easier to create auto expiring cache keys and only uses sweepers when I want to tie into very specific events. I'd also argue that any well designed system does not need sweepers (or at least in very minimally).

Now you should have a good grasp on how the Rails caching methods work. We've covered how fragment caching uses the current view to generate a cache key. We introduced the concept of auto expiring cache keys using ActiveRecord#cache_key to automatically expire cached content. We introduced action caching and how it uses url_for to generate a cache key. Then we covered how you can pass things into url_for to generate a time stamped key to expire actions automatically. Now that we understand these lower levels we can move up to page caching and HTTP caching.

Page Caching

Page caching bypasses the entire application by serving up a file in /public from disk. It is different from action or fragment caching for a two reasons: content is not stored in memory and content is stored directly on the disk. You use page caching the same way you use action caching. This means you can use sweepers and and all the other things associated with them. Here's how it works.

  • Webserver accepts an incoming request: GET /posts

  • File exists: /public/posts.html

  • posts.html is returned

  • Your application code is never called.

Since pages are written like public assets they are served as such. You will expliclity have to expire them.

Warning

Forgetting to expire pages will cause you grief because you application code will not be called.

Here's an example of page caching:

PostsController < ApplicationController
  caches_page :index

  def index
    # do stuff
  end

When the server receives a request to GET /posts it will write the response from the application to /public/posts.html. The .html part is the format for that request. For example you can use page caching with JSON. GET /posts.json would generate /public/posts.json.

Page caching is basically poor man's HTTP caching without any real benefits. HTTP caching is more useful.

I've not covered page caching in much depth because it's very likely that if you're reading this page caching is not applicable to your application. The Rails Guides cover page caching in decent fashion. Follow up there if you need more information.

HTTP Caching

HTTP caching is the most complex and powerful caching strategy you can use. With great power comes great responsiblity. HTTP caching works at the protocol level. You can configure HTTP caching so the browser doesn't even need to contact your server at all. There are many ways HTTP caching can be configured. I will not cover them all here. I will give you an overview on how the system works and cover some common use cases.

How It Works

HTTP caching works at the protocol level. It uses a combination of headers and response codes to indicate weather the user agent should make a request or use a locally stored copy instead. The invalidation or expiring is based on ETags and Last-Modified timestamps. ETag stands for "entity tag". It's a unique fingerprint for this request. It's usually a checksum of the response body. Origin servers (computers sending the source content) can set either of these fields along with a Cache-Control header. The Cache-Control header tells the user agent what it can do with this response. It answers questions like: how long can I cache this for and am I allowed to cache it? When the user agent needs to make a request again it sends the ETag and/or the Last-Modified date to the origin server. The origin server decides based on the ETag and/or Last-Modified date if the user agent can use the cached copy or if it should use new content. If the server says use the cached content it will return status 304: Not Modified (aka fresh). If not it should return a 200 (cache is stale) and the new content which can be cached.

Let's use curl to see how this works out:

$ curl -I http://www.example.com
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-length: 822
Content-Type: text/html
Date: Mon, 09 Jul 2012 22:46:29 GMT
Last-Modified: Mon, 09 Jul 2012 21:22:11 GMT
Status: 200 OK
Vary: Accept-Encoding
Connection: keep-alive

The Cache-Control header is a tricky thing. There are many many ways it can be configured. Here's the two easiest ways to break it down: private means only the final user agent can store the response. Public means any server can cache this content. (You know requests may go through many proxies right?). You can specify an age or TTL. This is how long it can be cached for. Then there is another common situation: Don't check with the server or do check with the server. This particular Cache-Control header means: this is a private (think per user cache) and check with the server everytime before using it.

We can trigger a cache hit by sending the apporiate headers with the next request. This response only has a Last-Modified date. We can send this date for the server to compare. Send this value in the If-Modified-Since header. If the content hasn't changed since that date the server should return a 304. Here's an example using curl:

$ curl -I -H "If-Modified-Since: Mon, 09 Jul 2012 21:22:11 GMT" http://www.example.com
HTTP/1.1 304 Not Modified
Cache-Control: max-age=0, private, must-revalidate
Date: Mon, 09 Jul 2012 22:55:53 GMT
Status: 304 Not Modified
Connection: keep-alive

This response has no body. It simply tells the user agent to use the locally stored version. We could change the date and get a different response.

$ curl -I -H "If-Modified-Since: Sun, 08 Jul 2012 21:22:11 GMT" http://www.example.com
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-length: 822
Content-Type: text/html
Date: Mon, 09 Jul 2012 22:57:19 GMT
Last-Modified: Mon, 09 Jul 2012 21:22:11 GMT
Status: 200 OK
Vary: Accept-Encoding
Connection: keep-alive

Caches determine freshness based on the If-None-Match and/or If-Modified-Since date. Using our existing 304 response we can supply a random etag to trigger a cache miss:

$ curl -I -H 'If-None-Match: "foo"' -H "If-Modified-Since: Mon, 09 Jul 2012 21:22:11 GMT" http://www.example.com
HTTP/1.1 304 Not Modified
Cache-Control: max-age=0, private, must-revalidate
Date: Mon, 09 Jul 2012 22:55:53 GMT
Status: 304 Not Modified
Connection: keep-alive

Etags are sent using the If-None-Match header. Now that we understand the basics we can move onto higher level discussion.

Rack::Cache

HTTP caching is implemented in the webserver itself or at the application level. It is implemented at the application level in Rails. Rack::Cache is a middleware that sits at the top of the stack and intercepts requests. It will pass requests down to your app and store their contents. Or will it call down to your app and see what ETag and/or timestamps it returns for validation purposes. Rack::Cache acts as a proxy cache. This means it must respect caching rules described in the Cache-Control headers coming out of your app. This means it cannot cache private content but it can cache public content. Cachable content is stored in memcached. Rails configures this automatically.

I'll cover one use case to illustrate how code flows through middleware stack to the actual app code and back up. Let's use a private per user cache example. Here's the cache control header: max-age-0, private, must-revalidate. Pretend this is some JSON API:

  1. The client sends initial request to /api/tweets.json

  2. Rack::Cache sees the request and ignores it since there is no caching information along with it.

  3. Application code is called. It returns a 200 response with a date and the some Cache-Control header.

  4. The client makes another request to /api/tweets.json with an If-Modified-Since header matching the date from the previous request.

  5. Rack::Cache sees that his request has cache information associated with it. It checks to see how it should handle this request. According to the Cache-Control header it has expired and needs to be checked to see if it's ok to use. Rack::Cache calls the application code.

  6. Application returns a response with the same date.

  7. Rack::Cache recieves the response, compares the dates and determines that it's a hit. Rack::Cache sends a 304 back.

  8. The client uses response body from request in step 1.

HTTP Caching in Rails

Rails makes it easy to implement HTTP caching inside your controllers. Rails provides two methods: stale? and fresh_when. They both do the same thing but in opposite ways. I prefer to use stale? because it makes more sense to me. stale? reminds more of Rails.cache.fetch so I stick with that.

stale? works like this: checks to see if the incoming request ETag and/or Last-Modified date matches. If they match it calls head :not_modified. If not, it can call a black of code to render a response. Here is an example:

def show
  @post = Post.find params[:id]
  stale? @post do
    respond_with @post
  end
end

Using stale? with an ActiveRecord object will automatically set the ETag and Last-Modified headers. The Etag is set to a MD5 hash of the objects cache_key method. The Last-Modified date is set to the object's updated_at method. The Cache-Control header is set to max-age=0, private, must-revalidate by default. All these values can be changed by passing in options to stale? or fresh_when. The methods take three options: :etag, :last_modified, and :public. Here are some more examples:

# allow proxy caches to store this result
stale? @post, :public => true do
  respond_with @post
end

# Let's stay your posts are frozen and have no modifications
stale? @post, :etag => @post.posted_at do
  respond_with @post
end

Now you should understand how HTTP caching works. Here are the important bits of code inside Rails showing it all works.

# File actionpack/lib/action_controller/metal/conditional_get.rb, line 39
def fresh_when(record_or_options, additional_options = {})
  if record_or_options.is_a? Hash
    options = record_or_options
    options.assert_valid_keys(:etag, :last_modified, :public)
  else
    record  = record_or_options
    options = { :etag => record, :last_modified => record.try(:updated_at) }.merge(additional_options)
  end

  response.etag          = options[:etag]          if options[:etag]
  response.last_modified = options[:last_modified] if options[:last_modified]
  response.cache_control[:public] = true if options[:public]

  head :not_modified if request.fresh?(response)
end

Here is the code for fresh?. This code should help you if you are confused on how resquests are validated. I found this code much easier to understand than the official spec.

def fresh?(response)
  last_modified = if_modified_since
  etag          = if_none_match

  return false unless last_modified || etag

  success = true
  success &&= not_modified?(response.last_modified) if last_modified
  success &&= etag_matches?(response.etag) if etag
  success
end

Using Caching Strategies

Using caching effectively can be tricky and frustrating. The best solution (like most things in programming) is to take a little bit of everything to make your own secret sauce. Here are some general recommendations:

  • Use HTTP caching everywhere. This cuts down on bandwidth. No other caching strategy can do this. Users on subpar connections (read: mobile users) will see a major benefit because they will not have to download the entire page again. This can amount of MB of savings when interacting with specific applications.

  • You can ignore page and action caching when using HTTP caching. They do the same thing but less effectively.

  • Use the Russian doll approach when rendering complex views

  • Use Rails.cache inside models to improve performance of common and costly operations.

  • Use auto expiring cache keys for everything.

Let's take that advice and apply a multi-layered strategy to a blog.

Our Blog

Our blog is simple. It has a main page which lists all the posts with their meta data. The post page has the entire content, a list of comments, and some general sidebar type stuff. This is a very common layout. We'll use HTTP caching in the front and Russian doll fragment caching in the back. This is fastest way you can do it because: initial requests will fill the cache with all the individual HTML fragments then that response will be cached locally. Subsequent invalid requests will be composed of existing cached fragments saving time in HTML generation and validation.

Here's our initial controller:

PostsController < ApplicationController
  respond_to :html

  def index
    @posts = Post.scoped # this is important!
    respond_with @posts
  end

  def show
    @post = Post.find params[:id]
    respond_with @post
  end
end

Here's the initial views:

<% @posts.each do |post| %>
  <p>
    <%= link_to post.author, author_path(post.author) %>
    <%= link_to post.title, post_path(post) %><br \>
    <%= truncate post.body %>
    <%= post.comments.count %><%= pluralize "comments", post.comments.count %>
  </p>
<% end %>
<% div_for @post do %>
  <h1><%= post.title %></h1>
  <%= complex_format @post.body %>
  <% render :partial => 'signature', :locals => { :author => @post.author }} %>

  <h2>Comments</h2>
  <% @post.comments.each do |comment| %>
    <p><%= comment %></p>
  <% end %>
<% end %>

<% render 'sidebar' %>

Now that we have the initial controller and views we can start to make them more performant. I've taken some liberties with the view code to introduce more content and render a partial. Rendering partials can be expensive due to binding creation and other things. These views are not inherently complex. They do provide a simple use case to see how methods can be applied.

Step 1: Fragment Caching the View

Let's start with easiest things to do. Cache the individual components of the view.

<% @posts.each do |post| %>
  <% cache post, 'main-listing' %>
    <p>
      <%= link_to post.author, author_path(post.author) %>
      <%= link_to post.title, post_path(post) %><br \>
      <%= truncate post.body %>
      <%= post.comments.count %><%= pluralize "comments", post.comments.count %>
    </p>
  <% end %>
<% end %>
<% div_for @post do %>
  <% cache [post, 'main-content'] %>
    <h1><%= post.title %></h1>
    <%= complex_format @post.body %>
    <% render :partial => 'signature', :locals => { :author => @post.author }} %>
  <% end %>

  <% cache [post, 'comments'] do %>
    <h2>Comments</h2>
    <% @post.comments.each do |comment| %>
      <p><%= comment %></p>
    <% end %>
  <% end %>
<% end %>

<% cache 'sidebar' %>
  <% render 'sidebar' %>
<% end %>

I've simply wrapped the individual sections in cache blocks. Each block uses auto expiring cache keys for the post with another string to indicate what it is. Now going to these pages would hit the cache and save some time.

Now apply the Russian doll technique. It's easy to see that a post's page would change when a new comment is added. The post's content hasn't changed though. complex_format may be an expensive operation that we don't want to perform again. We can cache one large chunk that will expire. The large chunk is composed of smaller cacheable chunks. What we can do now is wrap the views in one entire cache block.

<% cache @posts %>
  <% div_for @post do %>
    <% cache [post, 'main-content'] %>
      <h1><%= post.title %></h1>
      <%= complex_format @post.body %>
      <% render :partial => 'signature', :locals => { :author => @post.author }} %>
    <% end %>

    <% cache [post, 'comments'] do %>
      <h2>Comments</h2>
      <% @post.comments.each do |comment| %>
        <p><%= comment %></p>
      <% end %>
    <% end %>
  <% end %>

  <% cache 'sidebar' %>
    <% render 'sidebar' %>
  <% end %>
<% end %>

Now we have views composed of individual chunks! It's overkill in this use case but the point is illustrated. Now let's move onto HTTP caching in the controller.

Step 2: HTTP Caching in the Controller

def show
  @post = Post.find params[:id]

  if stale? @post do
    respond_with @post
  end
end

Voila! That was easy. We have HTTP caching and fragment caching for the individual post pages. Now we can tackle the index pages. These are slightly more complex because the view depends on many records. The number of comments on each posts is displayed in the list. What happens when one post gets a new comment? Well we have to display something differently.

Step 3: Caching Views Generated from Arrays

We need to generate a cache key that factors in all the records. The cache key also needs to be auto expiring. Let's define a method on Post that does just that.

require 'digest/md5'

class Post
  def self.cache_key
    Digest::MD5.hexdigest "#{maximum(:updated_at)}.try(:to_i)-#{count}"
  end
end

This method is easy to understand: generate a unique hash based on the most recently updated record and how many records are present. This makes the key auto expire when a record is added, updated (given updated_at is changed), and a record is deleted. We factor in the count to make deletions work. Deleting records would not change the key without count. Assume you deleted the very first post. The most recently updated post would set the timestamp and it would cause a hit. We wrap the whole entire thing in a MD5 so any change will generate a unique cache key. Now we can update the index view.

<% cache @posts # this works now because @posts defines cache_key %>
  <% @posts.each do |post| %>
    <% cache [post, 'main-listing'] %>
      <p>
        <%= link_to post.author, author_path(post.author) %>
        <%= link_to post.title, post_path(post) %><br \>
        <%= truncate post.body %>
        <%= post.comments.count %><%= pluralize "comments", post.comments.count %>
      </p>
    <% end %>
  <% end %>
<% end %>

Implementing HTTP caching is easy as well. It is not reliable to use timestamps (Last-Modified and If-Modified-Since) because of the issues previously described. It's easier to use etags in this case. ETags will ensure that each request to GET /posts will have unique fingerprint based on all the underlying posts. We'll use the cache_key method for the ETag:

def index
  @posts = Post.scoped
  if stale? @posts do
    respond_with @posts
  end
end

And that's all we have to do there.

Step 5: Comments Touch Posts

Comments must touch Posts to make everything work. This code will change a post's timestamp whenever a comment is updated. This will also change the cache key for arrays of posts.

class Comment < ActiveRecord::Base
  belongs_to :post, :touch => true
end

Now we've acheived the holy grail. Any change in the data layer will auto expire everything in the view layer. We don't have to handle the on the Hard Problems™ in computer science: cache invalidation.

Handling Code Changes

Everything described so far works perfectly when the data changes. What happens when you want to deploy a new version of your blog? This is a very good question. Without any additional configuration you'd have key collisons. The cache doesn't know that is a difference between today's deploy and yesterday's deploy.

We can solve this problem with auto-expiring keys. All the cache keys must factor in some meta data about the current deploy. All higher level cache calls eventually go through ActiveSupport::Cache.expand_cache_key as described earlier. This method checks if ENV["RAILS_CACHE_ID"] or ENV["RAILS_APP_VERSION"] is present and factors them into the key. All we have to do from a deployment perspective is update these environment variables after each deploy. The easiest way is to set it to the SHA of the current commit.

Here's a command you can execute during your deployment:

$ export RAILS_APP_VERSION=$(git rev-parse --short HEAD)

This will not work for all deployments but you get the idea. This has an interesting side effect. If you rollback your deployment, it will switch back to the cache for that version.

Static Assets

Static assets are things that don't change. These are things like JavaScript files, style sheets, and images. Caching and serving static assets is a big aspect of any web application. They can be served directly through Rack/Thin/Unicorn, through a web server like Nginx, or through a CDN like Cloudflare. These options have been listed in slowest first order. The objective for all of these strategies is to serve an asset that can be cached indefinitely until the asset has changed.

Static Asset Caching Strategies

This can be done through a combination of a few methods. Far future expires (FFE) is one method. FFE essentially set the age to 0 or set the expire time to the maximum possible value (usually a year). Sprockets is the asset server in Rails 3.1+. It uses fingerprinting (essentially etags) to generate unique URLs for each asset. Fingerprinting generates a URL like this: /assets/application-9ea8d161dc03c8b77398d9e6e8ec452f.js. All the static asset helpers in Rails append the fingerprint in production. So you'd see: /assets/images/logo-9ea8d161dc03c8b77398d9e6e8ec452f.png. Assets can be requested in two different ways: with and without the fingerprint. If the fingerprint (trailing hash) is given the response is served with following headers:

Cache-Control: public, max-age=31536000
ETag: "fingerprint"
Last-Modified: timestamp

If the file is requested without the fingerprint:

ETag: "fingerprint"
Last-Modified: timestamp
Cache-Control: public, must-revalidate

The content is served with the must-revalidate flag because /assets/application.js could refer to any fingerprint version. Setting must-revalidate forces the user agent to check with the origin server and make sure the content is the same.

Handling Static Assets in Production

All assets need to be precompiled before deploying. All future discussion assumes they are. This dumps all the assets into /public/assets. This also means that all requests to /assets/application-fingerprint.js are no longer going through your application code. Remember index.html? That pesky file with every new Rails app that you have to delete? Assets are just like that. Rails does not serve static assets by default in production. Here are some common situation and ways to do this.

Static Assets on Heroku (or any direct ruby process)

Heroku does not serve your application through a web server. Your application has do all that work and handle responses. The Rails guides describe how to configure Apache/Nginx, but don't describe how to handle the situation yourself. Rails uses ActionDispatch::Static to serve /public. This middleware is active then config.serve_static_assets is true. ActionDispatch::Static takes an argument one argument: the value for the Cache-Control header. Annoyingly, this is not set by default in current rails applications. Older rails applications may have config.static_cache_control present in production.rb. These steps assume all your assets are finger printed.

  1. Enable config.serve_static_assets in production.rb

  2. Set config.static_cache_control to public, max-age=31536000

  3. Redeploy

Now all requests to /public/**/*.* will be publicly cached with a far future expire. This is the slowest way, but the only web possible if you don't have access to a web server.

Static Assets with a Nginx/Apache

Each CDN is different. They use some sort of internal and external caching to deliver your assets quickly. I will not cover this in depth because it's outside the scope of this guide, but all of them use HTTP caching as described earlier.

Moving Away from the HTTP Request

Everything we've done so far has been in the HTTP request context. Complex applications live outside HTTP. They have background processes that interact with external systems and update data. This is a problem when using standard Rails caching. This section is about handling those problems.

Setting the Stage

We know that action caching is dependent on URLs. Fragment caching is dependent on the view being rendered. However, we know that both of these methods use Rails.cache under the covers to store content. We can use Rails.cache any where in our code. Unlike caches_path, caches_action and cache that don't hit the cache if perform_caching is set to false, the Rails.cache methods will always touch the cache. Ideally, it would be nice to create a simple observer for our models. If would be cool if we had a class like this:

class Cache 
  def self.expire_page(*args)
    # do stuff
  end

  def self.expire_action(*args)
    # do stuff
  end

  def self.expire_fragment(*args)
    # do stuff
  end
end

Then we can use this utility class anywhere in our code to expire different things we have cached. First, we need to be able to generate URLs from something other than a controller. You may be familiar with this problem. Mailers are not controllers, but you can still generate URLs. You need a host name to generate URLs. The controller has this information because they accept HTTP requests which contain that information. Mailers do not. That's why the host name must be configured in the different environments. We can create a frankenstein class that takes parts of ActionMailer to generate URLs. Once we can generate URLs we can expire pages and actions. URL helpers are in this module Rails.application.routes.url_helpers. We also need a class level variable for the host name. Here's what we can do so far:

class Cache
  include Rails.application.routes.url_helpers # for url generation

  def self.default_url_options
    ActionMailer::Base.default_url_options
  end

  def expire_action(*args)
    # do stuff
  end

  def expire_fragment(*args)
    # do stuff
  end
end

Now we can pull in some knowledge on how the cache system works to fill in the gaps. Some of this comes from reading the various source files and observation in generating the cache keys. Here is the complete class:

class Cache
  include Rails.application.routes.url_helpers # for url generation

  def self.default_url_options
    ActionMailer::Base.default_url_options
  end

  def expire_action(key, options = {})
    expire(key, options)
  end

  def expire_fragment(key, options={})
    expire(key, options)
  end

  private
  def caching_enabled?
    return ActionController::Base.perform_caching
  end

  def expire(key, options = {})
    return unless caching_enabled?
    Rails.cache.delete expand_cache_key(key), options
  end

  def expand_cache_key(key)
    # if the key is a hash, then use url for
    # else use expand_cache_key like fragment caching
    to_expand = key.is_a?(Hash) ? url_for(key).split('://').last : key
    ActiveSupport::Cache.expand_cache_key to_expand, :views
  end
end

Since action and fragment caching all use Rails.cache under the hood, we can simply generate the keys ourselves and remove them manually – all without the fuss of HTTP requests. Now you can create an initializer to define a method on your application namespace so it's globally accessible. I like this way because it's easy to reference in any piece of code.

# config/initializers/cache.rb
require 'cache'

module App # whatever you application module is
  class << self
    def cache
      @cache ||= Cache.new
    end
  end
end

Now we can merrily go about our business expiring cached content from anywhere. Here are some examples:

App.cache # reference to a Cache instance

App.cache.expire_fragment @post
App.cache.expire_fragment [@post, 'sidebar']
App.cache.expire_fragment 'explicit-key'

# in a controller
App.cache.expire_fragment post_url(@post)
# Have to pass in the hash since it's most likely
# that you won't have access to the url helpers
# in whatever scope your're in.
App.cache.expire_action :action => :show, :controller => :posts, :id => @post, :tag => @post.updated_at.to_i

The expire_fragment and expire_action methods work just like the ones described in the Rails Guides. Only difference is, you can use them anywhere. Now we can easily call this code in an observer. The observer events will fire every time they happen anywhere in the codebase. Here's an example. I am assuming a todo is created outside an HTTP request through a background process. The observer will capture the event.

class TodoObserver < ActivRecord::Observer
  def after_create
    App.cache.expire_fragment :controller => :todos, :action => :index
  end
end

The beauty here is that we can use this code anywhere. If you have more complicated cache expirations you may have to use a background job. This may not be acceptable because of processing time, but in some situations you can afford a sweeping delay if the sweeping process takes a long time. You could easily use this code with Sidekiq or Resque if needed. After all, the generated rails code does reference a cache observer--now you know how to write one.

Tag Based Caching

Tag based caching is a way to solve the second hard problem in computer science: cache invalidation. I was working on a complex application that generated a ton of HTML. It was very repetitive in nature but highly associative. The same data would be displayed on many different pages. HTML fragments may need to reference many different objects to make it all work. At this scale I could no longer think of individual fragments. I could only think of the objects them selves. I simply wanted to express this statement: expire everything associated with this contact.

Here's what I was dealing with:

  1. Maintain control over how long things are cached

  2. Large number of different associations. Actions or fragments no longer related to a specific resource.

  3. Content could be invalidated through HTTP requests or any number of background process.

  4. Hard to maintain specific keys. I thought of it as "resources".

Enter Cashier

There is a ton of cached content in the system. Many different actions and fragments. There was also a cache hierarchy. Expiring a specific fragment would have to expire an action (so a cache miss would occur when a page was requested thus, causing the new fragment to be displayed) while other things on pages are still cached. One question to ask, is how can I expire groups of things based on certain events? Well, first you need a way to associate different keys. Once you can associate different keys, then you can expire them together. Since you're tracking the keys being sent to Rails.cache, you can simply use Rails.cache to delete them. All of this is possible through one itty-bitty detail of the Rails caching system.

You may have noticed something in the Cache class in the previous section. There is a second argument for options. Anything in the option argument is passed to the cache store. This is where can tie in the grouping logic.

Through all of this trickery, you'll be able to express this type of statement:

App.cache.expire_tag 'stats' 
App.cache.expire_tag @account

The content could from anywhere, but all you know is that's stale.

This is exactly where Cashier comes in. It (is my gem) that allows you associate actions and fragments with one or more tags, then expire based of tags. Of course you can expire the cache from anywhere in your code. Here are some examples:

caches_action :stats, :tag => proc {|c|
  "account-#{Account.find(c.params[:id]).id}"
}

caches_action :show, :tag => 'account'
caches_cation :show, :tag => %w(account customer)

<%= cache @post, :tag => 'customer' do %>

Then you can expire like this:

Cashier.expire 'account' # wipe all keys tagged 'account'

I highly recommend you checkout Cashier. It may be useful in your application especially if you have complicated relationships and high performance requirements.

Fast JSON APIs

All I care about is fast JSON API's. That's all I work on and that's what I devote all my energy to. We can use all the principles here to create a simple example of a fast API.

Structuring a JSON API

This example assumes a few things:

  • ActiveRecord backed objects

  • Server is essentially a dumb store (no logic in controllers)

  • ActiveModel::Serializers for JSON generation

  • HTTP caching in the controllers

  • Fragment caching for JSON

Classes

# monkey patch ActiveRecord to add methods for caching
# same code from earlier

module ActiveRecord
  class Base
    def self.cache_key
      Digest::MD5.hexdigest "#{scoped.maximum(:updated_at).try(:to_i)}-#{scoped.count}"
    end
  end
end
# Only GET method are implemented in this example

class ResourceController < ApplicationController
  responds_to :json

  def index
    # uses our cache_key method defined on ActiveRecord::Base to 
    # set the etag
    if stale? collection do
      # Use cached JSON from individual hashes to render a collection
      respond_with collection
    end
  end

  def show
    # uses resource.updated_at to set the Last-Modified header
    # uses resource.cache_key to set the ETag
    if stale? resource do
      # Use cached JSON if possible
      respond_with resource
    end
  end
end
# Uses russian doll technique
class ApplicationSerializer < ActiveModel::Serializer
  delegate :cache_key, :to => :object

  # Cache entire JSON string
  def to_json(*args)
    Rails.cache.fetch expand_cache_key(self.class.to_s.underscore, cache_key, 'to-json') do
      super
    end
  end

  # Cache individual Hash objects before serialization
  # This also makes them available to associated serializers
  def serializable_hash
    Rails.cache.fetch expand_cache_key(self.class.to_s.underscore, cache_key, 'serilizable-hash') do
      super
    end
  end

  private
  def expand_cache_key(*args)
    ActiveSupport::Cache.expand_cache_key args
  end
end

Background Cache Warming

We've consolidated all the JSON generation into individual classes. Since the API only returns JSON we can generate that JSON silently in the background to warm the caches. This won't do anything about HTTP caching but it will make initial requests faster since JSON will be cached. Here's a simple Sidekiq worker:

class CacheWarmer
  include Sidekiq::Worker

  def perform
    Post.find_each do |post|
      serializer = post.active_model_serializer.new post
      # This wil cache the JSON and the hash it's generated from
      serializer.to_json
    end
  end
end

And that's all there is too it folks! It's not complicated but it will make your API significantly faster.

Tips and Tricks

This section is for random tips and tricks that don't really belong in any other parts. They are related to any caching method.

CSRF and form_authenticty_token

Rails uses a Cross Site Request Forgery (CSRF) token and a form authentic token to protect your application against attacks. These are generated per request and each pages get unique values each time. protect_from_forgery is added by default to ApplicationController. You may have run into these problem before. You may have tried to submit a POST and received an Unauthorized response. This is the form_authenticity_token in action. You can fiddle with it and see what happens to your application.

These tokens cause problems (depending on what Rails version) you're using with cached HTML. Caching a form will generate unauthorized errors because the tokens were for a different session or request. There are parts of the cached pages that need to be replaced with new values before the application can be used. This is a simple process, but it will take another HTTP request.

You'll need to create a controller to serve up some configuration related information that's never cached. That way, a cached action will load, then a separate request will be made for correct tokens.

You need to create a new controller that responds_to JSON and return some JSON to handle in a jQuery callack. Make sure this request authenticates the current user! It's also very important to not use JavaScript for this! Cookies are sent with every request to JS which may allow attackers to exploit your site.

Here's an abstract implemenation of the controller action:

# controller
def tokens
  authenticate! # do what you need to here
end

Now the view:

# tokens.json.erb

{
  "token": "<% Rack::Utils.escape_html(request_forgery_protection_token) %>",
  "param": "<% Rack::Utils.escape_html(form_authenticity_token) %>"
}

And the jQuery code:

$(function() {
  $.getJSON('/tokens.json', function(response) {
    $("meta[name='csrf-token']").attr('content', response.token);
    $("meta[name='csrf-param']").attr('content', response.param);
  });
})

See exploit here.

Bringing Caching into the Model Layer

Caching isn't just for views. Some database operations or methods may be computationally intensive. We can use Rails.cache inside the models to make them more efficient. Let's say you wanted to cached the listing of the top 100 posts on reddit:

class Post
  def self.top_100
    timestamp = Post.maximum(:updated_at)
    Rails.cache.fetch ['top-100', timestamp.to_i'].join('/') do
      order('vote_count DESC').limit(100).all
    end
  end
end

Dealing with Relative Dates (or other content)

Many Rails applications use distance_of_times_in_words throughout their application. This can cause major problems for any cached content with a date. For example, you have a fragment cached: that fragment was cached 1 month ago. 2 months ago, it's still in the cache. Since you stored a relative date in the cache, the fragment contains '1 month ago'. This is no good. You can solve this problem easily with JavaScript.

JavaScript is better for handling dates/times than Rails is. This is because Rails needs to know what the user's time zone is, then marshal all times into that time zone. JavaScript is better because it use the local time zone by default. How often do you want to display a time in a different zone than user's current locale? You can dump the UTC representation of the date into the DOM, then use JavaScript to parse them into relative or something like strftime. I've encapsulated this process in a helper in my Rails applications. Once all the data is in the DOM, you can do all the parsing in JavaScript.

def timestamp(time, options = {})
  classes = %w(timestamp)
  classes << 'past' if time.past?
  classes << 'future' if time.future?

  options[:class] ||= ""
  options[:class] += classes.join(' ')

  content_tag(:span, time.utc.iso8601, options)
end

Then, when the page loads use a library like date.js to create more user friendly dates.

Conclusion: Cashing Out

I've covered a ton of material in this article. I've given a through explanation of how all the Rails cache layers fit together and how to use the lowest level to it's full potential. I've provided a solution for managing the cache outside the HTTP request cycle as well as shown you how to bring caching into the model layer. This is not the be-all-and-end-all Rails caching. It is a in-depth look at caching in a Rails application. I'll leave you with a quick summary of everything covered and some few goodies.

HTTP Caching

  1. Very Handy! You should strive to reach this goal

  2. Cuts down on bandwidth when requests are fresh

  3. Cacheable responses are stored in Rack::Cache

  4. Uses ETag with If-None-Match and/or Last-Modified with If-Modified-Since date to check freshness

Page Caching

  1. The simplest that could possibly work

  2. Usually not applicable to any web application. Have a form? No good, the form_authenticity_token will be no good and Rails will reject it

Action Caching

  1. Most bang for the buck. Can usually be applied in many different circumstances

  2. Uses fragment caching under the covers

  3. Generates a cache key based off the current URL and whatever other options are passed in

  4. Get more mileage by caching actions with an composite timestamped key

Fragment Caching

  1. Good for caching reusable bits of HTML or JSON. Think shared partials or forms

  2. Use a good cache key for each cache block.

  3. Don't go overboard. Requests to memcached are not free. Maximize benefits by caching a small number of large fragments instead of a large number of small fragments.

General Points

  1. Don't worry about sweepers unless you have to

  2. Understand the limitations of Rail's HTTP request cycle

  3. Use cryptographic hashes to generate cache keys when permutations of input parameters are involved

  4. Don't be afraid to use Rails.cache in your data layer

  5. Tagged based caching is useful in certain situations

  6. Consolidate your cache expiration logic in one place so it's easily testable

  7. Test with caching turned on in complex applications

  8. Look into Varnish for more epic wins.

  9. belongs_to with :touch => true is your friend

  10. Use association timestamps

  11. Spend time upfront considering your cache strategy

  12. Be wary of examples with expire by regex. This only works on cache stores that have the ability to iterate over all keys. Memcached is not one of those.

  13. Use auto expiring keys for everything!

  14. Understand how cache validation and expiration works according to the HTTP caching spec.

  15. Cache your static assets! If possible, serve them through a CDN

  16. In rare situations, excessive calls to memcached may be slower than skipping it

  17. Consider the current locale when caching HTML

  18. Don't forget to set ENV['RAILS_APP_VERSION'] on every deploy

Site last updated on: December 11, 2012 at 10:21:28 PM PST