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

StoreConfigBest For
Memory Store:memory_storeDevelopment, single-server apps
File Store:file_storeSimple setups, no extra infrastructure
Redis Cache Store:redis_cache_storeProduction — fast, shareable across servers
Memcache Store:mem_cache_storeProduction — 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%.

Leave a Comment

Your email address will not be published. Required fields are marked *