Caching

Ruby on Rails · Advanced · 15 min

Caching is the cheapest way to make a slow Rails page fast, if keys and expiry are designed correctly.

Rails caching relies on key-based expiration: cache keys include the record's id and updated_at version, so when a record changes its key changes and stale entries simply stop being read. You rarely delete cache entries yourself.

Fragment caching in views:

<% cache article do %>
  <%= render article %>
<% end %>

Russian-doll caching nests fragments: the article fragment contains cached comment fragments. When a comment changes, belongs_to :article, touch: true bumps the article's updated_at, invalidating the outer fragment while unchanged inner fragments are reused. render partial: "comment", collection: comments, cached: true fetches a whole collection's fragments in one read.

Low-level caching stores any computed value:

Rails.cache.fetch(["stats", user], expires_in: 12.hours) { expensive_query }

It returns the cached value or runs the block and stores the result. Cache plain data (numbers, hashes, arrays of ids) rather than Active Record objects or relations.

HTTP caching: fresh_when(@article) or stale? set ETag and Last-Modified headers, and the server answers 304 Not Modified when the client already has the current version.

The store is configured per environment. Rails 8 uses Solid Cache in production, a database-backed store that can hold far more than memory-based caches; Redis and Memcached are alternatives. Caching is off in development until you run bin/rails dev:cache.

Example

<%# app/views/articles/show.html.erb %>
<% cache @article do %>                       <%# key: views/articles/42-20250601... %>
  <h1><%= @article.title %></h1>
  <%= render partial: "comments/comment",
             collection: @article.comments, cached: true %>
<% end %>

<%# app/models/comment.rb:
    belongs_to :article, touch: true   # comment change -> article updated_at bumps %>

<%# app/controllers/articles_controller.rb
  def show
    @article = Article.find(params[:id])
    fresh_when @article                       # 304 if the client's ETag matches
  end

  def stats
    @stats = Rails.cache.fetch(["article-stats", @article], expires_in: 1.hour) do
      { views: @article.views.count, readers: @article.views.distinct.count(:user_id) }
    end
  end
%>

Remember

  • Keys include updated_at, so changed records get new keys automatically
  • Russian-doll caching plus touch: true reuses unchanged inner fragments
  • Rails.cache.fetch caches computed values; fresh_when enables 304 responses

Watch out for

  • Caching relations or full Active Record objects instead of plain data, wasting space and returning stale or unloadable objects.

Check yourself

  1. A comment is edited. With touch: true on belongs_to :article, what happens to the cached article fragment?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: guides.rubyonrails.org/caching_with_rails.html