Rails Caching Strategies

· 5 min read · Updated March 17, 2026 · intermediate
ruby rails caching performance

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