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.