Fragment Caching in Rails: A Practical Guide to Faster Views

Fragment caching is one of the most effective ways to speed up a Ruby on Rails application without touching your database schema or rewriting business logic. At Essence Solusoft, we lean on it constantly across client projects and our own Essenify apps, wherever a view renders the same data repeatedly for different requests. This guide walks through what fragment caching actually does, how Russian doll caching builds on it, and how Rails 8’s default cache store, Solid Cache, changes the setup. Every technical claim below is checked against the official Rails Guides.

What Fragment Caching Actually Does

Fragment caching lets you wrap a piece of view logic in a cache block so it’s served from the cache store instead of being re-rendered on every request. It’s meant for pages where different parts have different caching needs, a product listing where the sidebar rarely changes but the product grid updates constantly, for example.

The basic syntax looks like this:

<% @products.each do |product| %>
 <% cache product do %>
   <%= render product %>
 <% end %>
<% end %>

On the first request, Rails writes a cache entry with a key that looks something like:

views/products/index:bea67108094918eeba42cd4a6e786901/products/1

The long string in the middle is a template tree digest, a hash computed from the contents of the view fragment itself. If you edit the partial’s HTML, that digest changes and the old cache entry is automatically treated as expired. Separately, a cache version derived from the record (based on its updated_at timestamp) is stored with the entry, so when the underlying product is touched, any cached fragment tied to the old version is ignored.

You can also cache conditionally with cache_if or cache_unless, useful when only certain users (like admins) should see a different, uncached version of a fragment.

By default, Action Controller caching (which includes fragment caching) is only enabled in production. To test it locally, run bin/rails dev:cache, which toggles caching on and off in development.

Collection Caching: Fewer Cache Reads for Free

If you’re rendering a collection with render, Rails can cache each item’s template and fetch them all from the cache store in a single batch instead of one lookup per item. You enable this with cached: true:

<%= render partial: ‘products/product’, collection: @products, cached: true %>

Items that are already cached get pulled back in one multi-fetch; anything not yet cached gets rendered, written to the cache, and picked up in a single batch on the next render. You can also customize the cache key, for example prefixing it with the current locale so different language versions of a page don’t collide:

<%= render partial:
         ‘products/product’,
collection: @products, cached: ->(product) { [I18n.locale, product] } %>

Russian Doll Caching: Nesting Fragments Inside Fragments

Once you’re caching individual fragments, the natural next step is nesting them, caching a fragment inside another cached fragment. This is what’s known as Russian doll caching. The benefit is that when one inner item changes, only that inner fragment needs to be regenerated; everything else nested inside the outer fragment can still be reused.

Take this example, where a product view renders its games:

<% cache product do %>

  <%= render product.games %>

<% end %>

Which in turn renders a cached fragment per game:

<% cache game do %>

  <%= render game %>

<% end %>

Here’s the catch worth knowing before you rely on this pattern: if an attribute on game changes, game‘s updated_at timestamp updates and its own cache expires correctly, but the outer product cache won’t know anything changed, because the product’s own updated_at hasn’t moved. The result is a stale outer fragment.

The fix is Rails’ touch option on the association:

class Product < ApplicationRecord

  has_many :games

end

class Game < ApplicationRecord

  belongs_to :product, touch: true

end

With touch: true, any change to a game record also updates its associated product’s updated_at, which correctly expires the outer cache along with the inner one.

Managing Cache Dependencies

Rails can usually figure out what a template depends on just by looking at calls to render inside it, this is called an implicit dependency, and it covers most common patterns like render partial: “comments/comment”, collection: commentable.comments or render(@topic).

Some dependencies can’t be inferred automatically, typically when rendering happens inside a helper method rather than directly in the view. In those cases you add an explicit dependency comment:

<%# Template Dependency: todolists/todolist %>

<%= render_sortable_todolists @project.todolists %>

You can also use a wildcard to depend on every template in a directory, useful for single-table-inheritance setups:

<%# Template Dependency: events/* %>

<%= render_categorizable_events @person.events %>

There’s also a third category: external dependencies, like a helper method whose output changes but whose usage inside the view doesn’t. Since Rails can’t detect that a plain Ruby method changed, you need to force the fragment’s digest to change some other way, commonly by adding a dated comment near the call so the template’s own hash shifts:

<%# Helper Dependency Updated: Jul 28, 2025 at 7pm %>

<%= some_helper_method(person) %>

Shared Partial Caching Across MIME Types

Rails also lets you share a single partial, and its cache, between formats like HTML and JavaScript. Because template resolvers only key off the template’s language extension (not its MIME type), the same cached partial can serve both an HTML request and a JS request:

render(partial: “hotels/hotel”, collection: @hotels, cached: true)

If you need to be explicit about which format a partial should render for, you can pass formats: directly.

Fragment Caching vs Low-Level Caching

Fragment caching is specifically for view output. When you need to cache a computed value, a query result, or data from an external API call rather than rendered HTML, that’s what low-level caching via Rails.cache.fetch is for:

class Product < ApplicationRecord

  def competing_price

    Rails.cache.fetch(“#{cache_key_with_version}/competing_price”, expires_in: 12.hours) do

      Competitor::API.find_price(id)

    end

  end

end

cache_key_with_version builds a key from the model’s class name, id, and updated_at, so the cache entry automatically invalidates whenever the record is updated. One thing to actively avoid: caching Active Record object instances directly. Instance attributes can drift out of sync with the database, and in development this pattern behaves unreliably with cache stores that reload code on changes. Cache primitive values, like IDs, instead, and re-query when you need the full records.

Solid Cache: The Default Cache Store Since Rails 8.0

Since Rails 8.0, Solid Cache has been the default Active Support cache store, a database-backed cache store designed around the assumption that SSD storage is fast enough and cheap enough to replace an in-memory store like Redis or Memcached for most applications. It uses a FIFO (first in, first out) eviction strategy rather than LRU, which is simpler and trades some cache-hit efficiency for the ability to hold a much larger dataset with fewer evictions overall.

If you’d rather not use it, you can skip it at app creation with rails new app_name –skip-solid, which also skips Solid Queue and Solid Cable (the other two components of the “Solid Trifecta”); each can be installed separately if you only want to opt out of one.

Solid Cache is configured through config/database.yml. A typical SQLite setup looks like:

production:

  primary:

    <<: *default

    database: storage/production.sqlite3

  cache:

    <<: *default

    database: storage/production_cache.sqlite3

    migrations_paths: db/cache_migrate

In production, Rails sets config.cache_store = :solid_cache_store by default. You can tune retention and size through config/cache.yml:

default: &default

  store_options:

    max_age: <%= 60.days.to_i %>

    max_size: <%= 256.megabytes %>

    namespace: <%= Rails.env %>

Solid Cache also supports sharding the cache across multiple databases for larger applications, and optional encryption for sensitive cached data via Active Record Encryption. By default, development mode uses :memory_store rather than Solid Cache, though you can switch it over explicitly if you want dev to mirror production caching behavior.

Other Cache Stores Rails Supports

Beyond Solid Cache, Rails ships with several other ActiveSupport::Cache::Store implementations you can swap in via config.cache_store:

  • :memory_store: keeps entries in the same Ruby process’s memory. It’s the default in development, but isn’t suitable for multi-process production setups (like clustered Puma) since processes can’t share cache data.
  • :file_store: stores entries on the filesystem, letting multiple processes on the same host share a cache, reasonable for low-to-medium traffic sites on one or two servers.
  • :mem_cache_store: uses memcached (via the dalli gem by default), historically the most common production cache store for high-performance, shared caching clusters.
  • :redis_cache_store: uses Redis, with automatic eviction when memory limits are hit. Rails’ guide specifically recommends a dedicated Redis instance for caching rather than reusing a persistent Redis server, since Redis doesn’t expire keys by default.
  • :null_store: disables caching per-request, meant for development and test environments where you need to see the effect of code changes without cached values getting in the way.

Conditional GET Support: A Different Kind of Caching

Rails also supports HTTP-level caching through conditional GETs, using ETags and Last-Modified headers so browsers and proxies can skip re-downloading unchanged content entirely. The stale? and fresh_when controller helpers implement this:

class ProductsController < ApplicationController

  def show

    @product = Product.find(params[:id])

    fresh_when last_modified: @product.published_at.utc, etag: @product

  end

end

Rails generates weak ETags by default (prefixed with W/), which allow semantically equivalent responses to share an ETag even if the bytes differ slightly. Strong ETags, which require byte-for-byte identical responses, matter mainly for range requests on large files like video or PDFs, and can be set explicitly with strong_etag.

Practical Takeaways

  • Start with fragment caching on your most expensive, least frequently changing views. Product grids, category pages, and dashboards with heavy partial rendering are typical candidates.
  • Use touch: true on associations wherever you nest cached fragments. Without it, Russian doll caching will silently serve stale data when a child record changes but its parent’s updated_at doesn’t move.
  • Don’t cache Active Record instances in low-level caching. Cache IDs or primitives and re-query.
  • On Rails 8+, you’re using Solid Cache by default in production. Know its FIFO eviction behavior and configure max_age/max_size in config/cache.yml rather than assuming Redis-like LRU behavior.
  • Reach for :redis_cache_store or :mem_cache_store if you’re running high-traffic, multi-server deployments and want a battle-tested shared cache outside your primary database.

Why This Matters for Rails Teams

Caching decisions compound. Getting fragment and Russian doll caching right early means fewer emergency performance fixes later, and understanding Solid Cache’s defaults matters now that it ships out of the box on every new Rails 8 app. At Essence Solusoft, we build this into how we architect both client platforms and our Essenify Shopify apps from day one rather than retrofitting it after a traffic spike.

Need help auditing or implementing a caching strategy for your Rails application? Get in touch with Essence Solusoft.

Sachin Gevariya

Sachin Gevariya

Sachin Gevariya is a Founder and Technical Director at Essence Solusoft. He is dedicated to making the best use of modern technologies to craft end-to-end solutions. He also has a vast knowledge of Cloud management. He loves to do coding so still doing the coding. Also, help employees for quality based solutions to clients. Always eager to learn new technology and implement for best solutions.

Say Hello To Essence

Tell us about your project and we are ready to transform your idea into stunning digital experiences

[contact-form-7 id="6"]
Contact form for CTA - Footer