Rails Caching Strategies
Caching is one of the most effective ways to improve Rails application performance. Without it, every request rebuilds views, queries the database, and processes data that rarely changes. With proper caching, you serve cached content instantly and reduce load on your database and application server.
This guide covers the main caching strategies in Rails: low-level caching, fragment caching, action caching, page caching, and the powerful Russian doll pattern.
Low-Level Caching
Low-level caching gives you fine-grained control over what gets cached. The Rails.cache interface provides a unified API across different storage backends (memory, Redis, Memcached, file store).
class User < ApplicationRecord
def self.top_contributors(limit: 10)
cache_key = "users/top_contributors/#{limit}"
Rails.cache.fetch(cache_key, expires_in: 1.hour) do
where('posts_count > 0').order(posts_count: :desc).limit(limit).to_a
end
end
end
This example caches the result of an expensive query for one hour. The cache_key ensures each combination of parameters gets its own cache entry.
# Reading from cache
Rails.cache.read("users/top_contributors/10")
# => [#<User id: 1>, #<User id: 5>, ...]
# Writing directly
Rails.cache.write("my_key", "some value", expires_in: 30.minutes)
# Deleting
Rails.cache.delete("my_key")
Choose your cache store in config/environments/production.rb:
config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") }
For development, the default memory store works fine. For production, Redis is the standard choice.
Fragment Caching
Fragment caching lets you cache portions of a view. When most of a page stays the same but a section changes per user or request, fragment caching shines.
<%# app/views/products/show.html.erb %>
<%= render @product %>
<% cache ['product', @product, 'details'] do %>
<div class="product-details">
<p>Category: <%= @product.category.name %></p>
<p>Reviews: <%= @product.reviews.count %></p>
</div>
<% end %>
<% if current_user %>
<%= render 'user_actions' %>
<% end %>
The cache key includes @product and a version string. When @product updates, the cache automatically expires.
You can also cache collections efficiently:
<% cache ['products', @productsMaximum_cache_key] do %>
<% @products.each do |product| %>
<%= render product %>
<% end %>
<% end %>
Action and Page Caching
Action caching stores the entire output of an action. It’s useful for pages that don’t need per-user customization.
class ProductsController < ApplicationController
# Cache the entire action output
caches_action :index, expires_in: 10.minutes
# Only cache for guest users
caches_action :show, if: -> { current_user.nil? }
# Skip caching for authenticated users
caches_action :dashboard, unless: -> { current_user.nil? }
# Clear specific action caches
def invalidate_index
expire_action action: :index
end
end
Page caching goes further—it serves static HTML files directly from the filesystem or CDN, bypassing Rails entirely:
class ProductsController < ApplicationController
caches_page :index, :show
# After creating/updating a product, expire the page cache
after_action :expire_cache, only: [:create, :update, :destroy]
private
def expire_cache
expire_page action: :index
expire_page action: :show, id: params[:id]
end
end
Note: Action and page caching are less common in modern Rails apps because they bypass the application entirely, making it harder to expire caches dynamically. Fragment caching offers more flexibility.
Russian Doll Caching
Russian doll caching nests cached fragments inside each other. When you update an inner fragment, outer fragments automatically expire. This creates a cascade of cache invalidation.
<%# Outer cache: product list %>
<% cache ['products', @products] do %>
<div class="products-grid">
<% @products.each do |product| %>
<%# Inner cache: individual product %>
<% cache ['product', product] do %>
<div class="product-card">
<h3><%= product.name %></h3>
<p><%= product.price %></p>
<%# Innermost: product reviews %>
<% cache ['product-reviews', product.reviews] do %>
<%= render product.reviews %>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
When a single review updates, only the innermost cache key expires. The product list and product card caches automatically expire because their dependencies changed.
This pattern dramatically reduces cache invalidation complexity:
# When a review is created
@review = Review.create!(...)
# => Cache key ['product-reviews', @review.product.reviews] changes
# => Outer product cache expires automatically
# => Products list cache expires automatically
Cache Expiration Strategies
Choosing when to expire caches is crucial. Here are the main approaches:
Time-based expiration is simple but can serve stale data:
Rails.cache.fetch(key, expires_in: 1.hour) { compute }
Version-based expiration ties cache to data versions:
cache_key = "product/#{product.id}/v#{product.version}"
Event-based expiration invalidates on specific actions:
# In an Observer or callback
after_save { Rails.cache.delete("products/#{id}") }
after_destroy { Rails.cache.delete("products/#{id}") }
Sweeper pattern for action caching:
# app/sweepers/product_sweeper.rb
class ProductSweeper < ActionController::Caching::Sweeper
observe Product
def after_save(product)
expire_cache_for(product)
end
def after_destroy(product)
expire_cache_for(product)
end
private
def expire_cache_for(product)
expire_action controller: :products, action: :index
expire_action controller: :products, action: :show, id: product.id
end
end
Practical Tips
Start with low-level caching for expensive database queries. Move to fragment caching for view sections that are expensive to render. Only use action or page caching when the entire response can be cached.
Monitor your cache hit rate in production. A low hit rate means your cache keys are too specific or expiration is too aggressive.
# Add to a Rails controller to monitor
def index
@products = Product.all
respond_to do |format|
format.html
format.json { render json: { cached: @products, stats: Rails.cache.stats } }
end
end
Avoid caching per-user data in shared caches like Redis unless you namespace keys by user ID. Otherwise, users might see each other’s data.
See Also
- Enumerable#reduce — The reduce method for accumulating values
- Enumerable#group_by — Grouping collections by key
- Struct — Lightweight Data Objects — Creating lightweight data structures