Caching in Rails
Caching stores the result of an expensive operation so the next request gets the answer instantly without repeating the work. A database query that takes 200ms can return in under 1ms from cache. Rails provides several caching strategies that you can apply at different levels of your application.
What Caching Solves
Without cache: User visits /products → Rails queries DB: SELECT * FROM products JOIN categories ... → Rails renders 500 rows of HTML → 400ms total → User 2 visits /products — same 400ms → User 3 visits /products — same 400ms With cache: User 1 visits /products → 400ms → result stored in cache User 2 visits /products → 2ms (served from cache) User 3 visits /products → 2ms (served from cache)
Enable Caching in Development
rails dev:cache
This toggles caching on or off in development mode. Production has caching enabled by default.
Fragment Caching — Cache Parts of a Page
Cache a portion of a view template. The cached block stores rendered HTML.
app/views/products/index.html.erb
<h1>All Products</h1>
<% cache "products-list" do %>
<% @products.each do |product| %>
<div>
<h2><%= product.name %></h2>
<p>$<%= product.price %></p>
</div>
<% end %>
<% end %>
The block renders once and stores the HTML. Subsequent requests skip rendering and serve the cached HTML directly.
Cache Keys — When to Invalidate
A cache key determines when the cache is fresh or expired. Use model objects as keys so the cache auto-expires when the record changes:
Cache a single record: <% cache @product do %> ← cache key: "products/3-20240115120000" (id + updated_at) ← invalidates automatically when product.updated_at changes <h2><%= @product.name %></h2> <p><%= @product.description %></p> <% end %> Cache a collection: <% cache ["products-v1", @products.cache_key_with_version] do %> ← invalidates when any product in the collection changes <% @products.each do |p| %> ... <% end %> <% end %>
Russian Doll Caching
Nest cache blocks inside each other. Inner caches invalidate individually without busting the outer cache:
<% cache @article do %>
<h1><%= @article.title %></h1>
<p><%= @article.body %></p>
<h2>Comments</h2>
<% @article.comments.each do |comment| %>
<% cache comment do %>
<p><%= comment.body %> — <%= comment.user.name %></p>
<% end %>
<% end %>
<% end %>
When a single comment changes, only that comment's cache invalidates. The article cache and other comments stay cached.
Low-Level Caching — Cache Any Value
Use Rails.cache to cache any computed value in your Ruby code:
# In a controller or service:
def featured_products
Rails.cache.fetch("featured_products", expires_in: 1.hour) do
Product.where(featured: true).includes(:category).limit(8).to_a
end
end
# Read and write manually:
Rails.cache.write("homepage_stats", { users: 1000, articles: 500 }, expires_in: 30.minutes)
Rails.cache.read("homepage_stats")
Rails.cache.delete("homepage_stats")
Rails.cache.exist?("homepage_stats")
Action Caching — Cache Entire Controller Actions
Cache the entire output of a controller action as a static page:
class ProductsController < ApplicationController
def index
@products = Rails.cache.fetch("all_products", expires_in: 15.minutes) do
Product.all.includes(:category).to_a
end
end
end
Cache Stores
| Store | Config | Best For |
|---|---|---|
| Memory Store | :memory_store | Development, single-server apps |
| File Store | :file_store | Simple setups, no extra infrastructure |
| Redis Cache Store | :redis_cache_store | Production — fast, shareable across servers |
| Memcache Store | :mem_cache_store | Production — alternative to Redis |
config/environments/production.rb
config.cache_store = :redis_cache_store, {
url: ENV["REDIS_URL"],
expires_in: 1.hour,
namespace: "myapp_cache"
}
HTTP Caching — Browser and CDN
Tell browsers and CDNs how long to cache a page:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
if stale?(last_modified: @article.updated_at, etag: @article)
respond_to do |format|
format.html
format.json { render json: @article }
end
end
# If not stale → returns 304 Not Modified with no body
end
end
Cache Invalidation Strategy
When does a cache need to clear?
Product name changes → cache @product (auto via updated_at)
New comment added → cache @article (touch: true on Comment model)
Price bulk update → delete("featured_products") manually
New user reaches top 10 → expires_in: 1.hour handles it passively
Force-expire a cache manually:
Rails.cache.delete("featured_products")
Rails.cache.delete_matched("products/*")
Caching is one of the highest-leverage performance improvements in Rails. Identify the slowest queries and most-visited pages first, add caching there, then measure the impact. Even a single well-placed Rails.cache.fetch can cut page load time by 80%.
