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.
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 theconfig.cache_controlandRack::Cache,Rack::ETag,Rack::ConditionalGetmiddlewares. These are used for HTTP caching.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.
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.
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.
Rails.cache: All cached content except cached pages are stored in theRails.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 ofActiveRecord::Baseobjects.
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.
You will not be able to ensure that content is kept in the cache as long as possible.
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
endThis 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
endNow 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 1msDamn. 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.cacheand 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 }
endThis 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:
caches_page,expire_pagecaches_action,expire_actioncache,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
endI 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 /postsFile exists:
/public/posts.htmlposts.htmlis returnedYour 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
endWhen 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:
The client sends initial request to
/api/tweets.jsonRack::Cachesees the request and ignores it since there is no caching information along with it.Application code is called. It returns a
200response with a date and the someCache-Controlheader.The client makes another request to
/api/tweets.jsonwith anIf-Modified-Sinceheader matching the date from the previous request.Rack::Cachesees that his request has cache information associated with it. It checks to see how it should handle this request. According to theCache-Controlheader it has expired and needs to be checked to see if it's ok to use.Rack::Cachecalls the application code.Application returns a response with the same date.
Rack::Cacherecieves the response, compares the dates and determines that it's a hit.Rack::Cachesends a304back.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
endUsing 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)
endHere 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.cacheinside 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
endHere'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
endVoila! 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
endThis 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
endAnd 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.
Enable
config.serve_static_assetsinproduction.rbSet
config.static_cache_controltopublic,max-age=31536000Redeploy
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
endThen 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
endNow 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
endSince 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
endNow 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
endThe 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:
Maintain control over how long things are cached
Large number of different associations. Actions or fragments no longer related to a specific resource.
Content could be invalidated through HTTP requests or any number of background process.
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::Serializersfor JSON generationHTTP 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
endAnd 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
endDealing 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)
endThen, 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
Very Handy! You should strive to reach this goal
Cuts down on bandwidth when requests are fresh
Cacheable responses are stored in
Rack::CacheUses
ETagwithIf-None-Matchand/orLast-ModifiedwithIf-Modified-Sincedate to check freshness
Page Caching
The simplest that could possibly work
Usually not applicable to any web application. Have a form? No good, the
form_authenticity_tokenwill be no good and Rails will reject it
Action Caching
Most bang for the buck. Can usually be applied in many different circumstances
Uses fragment caching under the covers
Generates a cache key based off the current URL and whatever other options are passed in
Get more mileage by caching actions with an composite timestamped key
Fragment Caching
Good for caching reusable bits of HTML or JSON. Think shared partials or forms
Use a good cache key for each cache block.
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
Don't worry about sweepers unless you have to
Understand the limitations of Rail's HTTP request cycle
Use cryptographic hashes to generate cache keys when permutations of input parameters are involved
Don't be afraid to use
Rails.cachein your data layerTagged based caching is useful in certain situations
Consolidate your cache expiration logic in one place so it's easily testable
Test with caching turned on in complex applications
Look into Varnish for more epic wins.
belongs_towith:touch => trueis your friendUse association timestamps
Spend time upfront considering your cache strategy
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.
Use auto expiring keys for everything!
Understand how cache validation and expiration works according to the HTTP caching spec.
Cache your static assets! If possible, serve them through a CDN
In rare situations, excessive calls to memcached may be slower than skipping it
Consider the current locale when caching HTML
Don't forget to set
ENV['RAILS_APP_VERSION']on every deploy





Add a comment



Add a comment