Caching Strategies for Rails
Last updated April 25, 2024
Table of Contents
Web applications will typically have a small handful of pages that take an exceptionally long time to load. On Heroku, long running requests can tie up your dynos and seriously effect application performance. Use New Relic to determine which pages and database requests are running slowly (see the Web Transactions and Database tabs). Examine the longest running requests. If they are caused by slow database or API transactions, then use low-level caching (i.e. Rails.cache.read/write/fetch
) to cache the information.
Heroku recommends against using ‘automagic’ caching libraries such as cache-money or cache_fu. We have spent time searching for an easy caching solution, but haven’t found one with which we are satisfied. In general caching is an application-specific endeavor. The below is a roadmap for approaching performance enhancement through caching on a Rails app running on Heroku.
Once you have configured your application to use memcached, Rails will automatically use it for both action and fragment caching.
HTTP caching
Caching using HTTP headers in Rails is a technique that can be easily applied to applications with little code modification and is covered in this separate article.
Page caching
Rails’ page_caching gem works by creating a file on the file system. Heroku has an ephemeral file store, so while page caching may appear to work, it won’t work as intended. You should instead use action or fragment caching, or alternatively use Rack::Cache as a reverse proxy to avoid requests to your apps at all.
Action caching
If your pages requires authentication or other before/after filters, the content can still be cached using the Rails’ action_caching gem. Action caching uses (and requires) a Memcache add-on.
Simply add caches_action :<action_name>
to your controller to turn on caching for specific actions. If your layout contains dynamic elements (such as your user’s name or email address in the header), you can render the layout dynamically while still caching the action’s contents. Use the :layout => false
flag to accomplish this. Finally, you can use the expire_action
command to remove the action from you cache when new data is written.
The following Rails controller code illustrates these concepts:
# products_controller.rb
class ProductsController < ActionController
before_filter :authenticate
caches_action :index
caches_action :show, :layout => false
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def create
expire_action :action => :index
end
end
(source: Rails Guides)
Fragment caching
Fragment caching is a great mechanism for caching widgets or partials in your application. Fragment caching uses (and requires) a Memcache add-on. For example, if your app lists products like this:
# index.html.erb
<%= render :partial => "product", :collection => @products %>
# _product.html.erb
<div><%= link_to product, product.name %>: <%= product.price%></div>
<div><%= do_something_comlicated%></div>
Then you could easily cache the partial for each individual product with fragment caching. Rails will automatically generate a cache key if you pass it an ActiveRecord object:
# _product.html.erb
<% cache(product) do %>
<div><%= link_to product, product.name %>: <%= product.price%></div>
<div><%= do_something_comlicated%></div>
<% end %>
Another fragment caching strategy is to cache widgets or other discrete portions of pages that do not need to be refreshed from your live datastore for each new page load. If for example the front page of your website listed top selling products, you could cache this fragment. Let’s assume that you want to refresh the information every hour:
# index.html.erb
<% cache("top_products", :expires_in => 1.hour) do %>
<div id="topSellingProducts">
<% @recent_product = Product.order("units_sold DESC").limit(20) %>
<%= render :partial => "product", :collection => @recent_products %>
</div>
<% end %>
Low level caching
Low-level caching entails using the Rails.cache
object directly to cache any information. Use it to store any data that is costly to retrieve and that can afford to be somewhat out-of-date. Database queries or API calls are common uses for this.
The most efficient way to implement low-level caching is using the Rails.cache.fetch
method. It will read a value from the cache if it available; otherwise it will execute a block passed to it and return the result:
>> Rails.cache.fetch('answer')
==> "nil"
>> Rails.cache.fetch('answer') {1 + 1}
==> 2
Rails.cache.fetch('answer')
==> 2
Consider the following example. An application has a Product model with a class method returning all out of stock items, and an instance method that looks up the product’s price on a competing website. The data returned by these methods would be perfect for low-level caching:
# product.rb
def Product.out_of_stock
Rails.cache.fetch("out_of_stock_products", :expires_in => 5.minutes) do
Product.all.joins(:inventory).conditions.where("inventory.quantity = 0")
end
end
def competing_price
Rails.cache.fetch("/product/#{id}-#{updated_at}/comp_price", :expires_in => 12.hours) do
Competitor::API.find_price(id)
end
end
Notice that in this final example, we generated a cache-key based on the model’s id and update_at attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general when you use low-level caching for instance level information you need to generate a cache key.
Further reading
- Caching with Rails by RailsGuides