• Home
  • About
  • Portfolio
  • Contact
CodeCurious
  • Home
  • About
  • Portfolio
  • Contact
Go Back

How Fragment Caching Works In Ruby On Rails

Learn how fragment caching works in Ruby on Rails to speed up page rendering and improve application performance.

Jean Emmanuel Cadet
By Jean Emmanuel Cadet • Ruby on Rails Developer

Last updated : Jul 08, 2026 • 14 min read

How Fragment Caching Works in Ruby on Rails

Last updated : Jul 08, 2026 • 14 min read

Share with friends

Every Rails developer eventually hits the same wall. Your application works great in development, the tests pass, and everyone is happy. Then real traffic shows up, and suddenly a page that used to load instantly takes two or three seconds to render. Users notice. Bounce rates climb. Your hosting bill quietly creeps up, too.

Performance matters more today than ever. People expect pages to load fast, and search engines factor speed into rankings. The good news is that you do not need to rewrite your entire application to fix slow rendering. One of the simplest and most effective tools in the Rails toolbox is fragment caching.

Fragment caching lets you store the rendered output of a piece of a view so Rails does not have to rebuild it on every request. It is built into Rails, requires very little setup, and can dramatically cut down response times on pages with expensive partials, loops, or database-heavy views. In this article, we will walk through exactly how fragment caching works, how to use it correctly, and how to avoid the mistakes that trip up a lot of developers when they first try it.


What Is Fragment Caching?

Fragment caching is a Rails caching strategy that stores the rendered HTML output of a specific part, or "fragment," of a view. Instead of caching an entire page, you cache just the pieces that are expensive to generate, like a list of blog posts, a sidebar of categories, or a user's activity feed.

This is different from other caching strategies you may have heard of:

  • Browser caching happens on the client side. The browser stores assets like images, CSS, and JavaScript, so it does not have to redownload them on every visit. Rails has no control over this once the response leaves the server.
  • Page caching used to be a Rails feature that cached an entire HTML page and served it directly from disk or a CDN, bypassing the Rails stack completely. It was removed from Rails core because it does not work well with dynamic, personalized content.
  • Fragment caching sits in between. It runs inside the Rails request cycle, so you still get authentication, authorization, and any per-user logic, but the expensive rendering work is skipped when a valid cache exists.

The benefits are significant. Fragment caching reduces database queries, cuts down on view rendering time, and lowers server load, all without sacrificing the flexibility of a fully dynamic application. It is one of the few optimizations that gives you a large performance win for a small amount of code.


How Fragment Caching Works

At the center of fragment caching is the cache helper, which you use directly inside your ERB templates. When Rails encounters a cache do ... end block, it checks whether a cached version of that fragment already exists in the configured cache store. If it does, Rails skips rendering the block entirely and just outputs the stored HTML. If it does not, Rails renders the block as normal, then stores the result for next time.

Rails identifies each cached fragment using a cache key. The key is a unique string built from things like the model's class name, its ID, and a timestamp representing when it was last updated. This is what allows Rails to know when a fragment is stale and needs to be regenerated instead of served from cache.

Cache expiration in Rails is mostly handled through key changes rather than explicit timers. Instead of saying "expire this cache after ten minutes," Rails generates a new cache key automatically whenever the underlying data changes. The old cached fragment simply becomes orphaned and is never requested again. Depending on your cache store, orphaned entries are eventually cleared out based on memory limits or a least-recently-used eviction policy.


Basic Fragment Caching Example

Here is what fragment caching looks like in its simplest form:

<% cache(@product) do %>
<div class="card">
<h2><%= @product.name %></h2>
<p><%= @product.description %></p>
<p>Price: <%= number_to_currency(@product.price) %></p>
</div>
<% end %>

Let's break this down line by line.

<% cache(@product) do %> tells Rails to look for a cached fragment tied to the @product object. Rails automatically builds a cache key using the product's class name, id, and updated_at timestamp.

Everything inside the block, the card markup showing the product name, description, and price, is what gets cached. On the first request, Rails renders this HTML and stores it. On every following request, as long as the product has not changed, Rails serves the stored HTML directly and skips the rendering work.

<% end %> closes the cache block.

This is a small example, but imagine this pattern applied to a product listing page with fifty items, each with associated reviews, images, and pricing calculations. The savings add up fast.


Cache Keys Explained

Cache keys are the backbone of fragment caching. Get them right, and your caching strategy works exactly as expected. Get them wrong, and you either serve stale content or barely cache anything at all.

Automatic Cache Keys

When you pass an ActiveRecord object into the cache helper, Rails automatically generates a key using the model name, the ID, and a version derived from updated_at. This means the moment a record is updated, its updated_at changes, which change the cache key, which naturally invalidates the old cache without you writing any extra code.

Using Models

You can pass a single record or a collection:

<% cache(@post) do %>
<%= render @post %>
<% end %>

Using Custom Cache Keys

Sometimes you need more control. Maybe the cache should also depend on the current user's role, or a locale, or some other piece of context. You can build a custom key manually:

<% cache(["product", @product, current_user.role]) do %>
<%= render @product %>
<% end %>

Rails will combine each element of the array into a single cache key, so different roles get different cached fragments even though they are viewing the same product.

Versioning

You can also add an explicit version string to a cache key. This is useful when you change your view templates and want to force old cached fragments to be considered invalid, even though the underlying data has not changed.

<% cache([@product, "v2"]) do %>
<%= render @product %>
<% end %>

Automatic Cache Invalidation

Rails handles most cache invalidation for you through a method called cache_key_with_version. Every ActiveRecord model responds to this method, and it combines the record's cache key with a version based on updated_at.

Here is the important part: you do not need to manually expire fragment caches when a record changes. As soon as you call .update or .save on a record and its updated_at column changes, the version component of the cache key changes too. The next time that view renders, Rails will not find a match for the new key, so it renders fresh content and stores it under the new key automatically.

product = Product.find(1)
product.update(price: 49.99)

After this update runs, any cached fragment built with cache(@product) for this record is effectively invalidated. The old cache entry still technically exists in the store, but nothing will ever ask for it again, so it just sits there until it is evicted.

This automatic behavior is one of the reasons fragment caching in Rails feels so low-friction compared to manually managing cache expiration in other frameworks.


Caching Collections

Fragment caching becomes even more valuable when applied to collections, like a list of blog posts or comments. Rails provides an optimized way to do this using render with a collection, combined with caching enabled at the partial level.

<%= render partial: "posts/post", collection: @posts, cached: true %>

Adding cached: true tells Rails to check the cache for each item in the collection. Items that are already cached get pulled directly from the cache store in a single batch read. Items that are not cached yet get rendered normally and then stored.

This is far more efficient than looping through the collection yourself and wrapping each item in a manual cache do block, because Rails can fetch multiple cache entries in one round trip to the cache store instead of one at a time.

Best practices for caching collections:

  • Keep each partial focused on a single record so cache keys stay predictable.
  • Avoid embedding request-specific logic, like flash messages, inside cached partials.
  • Make sure associations used inside the partial are eager loaded to avoid N+1 queries on cache misses.

Real-world example: a blog with a paginated list of twenty posts. Without caching, each request re-renders twenty partials, each pulling author data and category tags. With cached: true, only the posts that changed since the last request get re-rendered, and the rest are served from cache almost instantly.


Russian Doll Caching

Russian Doll Caching is a pattern where you nest cache blocks inside other cache blocks, similar to how Russian nesting dolls fit inside one another. The outer cache wraps a collection or a page section, while inner caches wrap individual records or smaller pieces.

<% cache(@post) do %>
<h1><%= @post.title %></h1>
<p><%= @post.body %></p>

<% cache(@post.comments) do %>
<%= render @post.comments %>
<% end %>
<% end %>

Here is why this pattern is powerful. If a single comment changes, only the inner comments cache needs to be invalidated and regenerated. The outer post cache stays valid because the post itself did not change. Rails achieves this through a feature where a parent object's cache key is influenced by its children through touch: true associations.

class Comment < ApplicationRecord
belongs_to :post, touch: true
end

With touch: true, updating a comment also updates the updated_at timestamp on its parent post, which changes the outer cache key just enough to invalidate the right layer without wiping out unrelated caches elsewhere on the page.

Advantages of Russian Doll Caching:

  • Only the smallest necessary piece of the page gets re-rendered.
  • Deeply nested view structures, like posts with comments with replies, stay fast even as content grows.
  • It scales naturally with your view hierarchy, since each partial manages its own cache independently.

Choosing a Cache Store

Fragment caching needs somewhere to actually store the rendered HTML. Rails supports several cache store options, and choosing the right one matters for both performance and reliability.

Memory Store

Stores cached data in the memory of the Ruby process itself. It is fast and requires no external dependencies, but it does not share cache between multiple server processes or machines. Good for development, not recommended for production apps running more than one process.

File Store

Writes cached fragments to disk. It works across a single server without extra infrastructure, but it does not scale well across multiple servers, and disk I/O can become a bottleneck under heavy traffic.

Redis Cache Store

A popular production choice. Redis is fast, supports expiration natively, and works well across multiple app servers since it is a shared external store. Configuring it looks like this in config/environments/production.rb:

config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }

Solid Cache

Solid Cache is the database-backed cache store introduced as part of the modern Rails 8 toolkit, alongside Solid Queue and Solid Cable. It stores cache entries in your existing relational database instead of requiring a separate Redis instance. For many applications, especially smaller to mid-sized ones, this removes an entire piece of infrastructure to manage while still giving you a shared cache across multiple servers.

config.cache_store = :solid_cache_store

Solid Cache is a great default for new Rails 8 applications that want production-grade shared caching without adding Redis to their stack on day one. If your application grows to a point where cache read and write volume becomes very high, Redis is still the option built for that kind of throughput.


Common Mistakes

Even a feature as simple as fragment caching has a handful of traps that catch developers off guard.

Forgetting cache invalidation. This usually happens when a cache key does not depend on the data it is displaying. If you cache based on a static string instead of the model itself, updates will never be reflected.

Caching highly dynamic content. Live counters, real-time notifications, or anything that changes every few seconds is a poor fit for fragment caching. The overhead of managing invalidation outweighs the benefit.

Large cache fragments. Caching an entire page inside one giant cache do block defeats the purpose. A single small change anywhere on the page invalidates the whole thing, so you lose most of the benefit of caching at all.

User-specific data. Caching content that includes a specific user's name, permissions, or personalized recommendations without including the user in the cache key will leak one user's data into another user's page.

Over-caching. Not every view needs caching. Wrapping simple, cheap views in cache blocks adds complexity and a small amount of overhead for reading and writing to the cache store, without any real performance gain.


Best Practices

  • Cache only expensive views. Profile first. If a partial renders in two milliseconds, caching it is not worth the added complexity.
  • Keep fragments small. Smaller, more granular fragments invalidate more precisely and stay useful longer.
  • Monitor cache hit rates. Tools like Redis's built-in stats or application performance monitoring services can show you how often your cache is actually being used versus missed.
  • Use proper cache keys. Always tie your keys to the data that determines what gets rendered, whether that is a model, a user role, or a locale.
  • Test cache behavior. Write tests that update a record and confirm the view output changes accordingly, so a bad cache key does not silently ship stale content to production.

Debugging Fragment Caching

When fragment caching does not behave the way you expect, Rails gives you a few tools to figure out what is going on.

Logging. In development, Rails logs cache activity directly to your console. You will see lines like Read fragment views/products/1 and Write fragment views/products/1, which tells you exactly when a fragment was a hit or a miss.

To make sure fragment caching logging is turned on in development, check config/environments/development.rb:

config.action_controller.perform_caching = true
config.cache_store = :memory_store

Development tools. You can inspect the cache store directly in a Rails console:

Rails.cache.exist?("views/products/1")
Rails.cache.read("views/products/1")

Common troubleshooting steps:

  • If a fragment never seems to update, check that the cache key actually includes the object whose changes you expect to trigger invalidation.
  • If nothing seems to be cached at all, confirm perform_caching is enabled for the environment you are testing in.
  • If you see unexpected content mixed between users, check whether user-specific data has leaked into a cache key that does not include the user.

Real-World Example

Let's look at a blog homepage that displays articles, their authors, and their categories. Here is a version without caching:

<% @posts.each do |post| %>
<div class="post-card">
<h2><%= post.title %></h2>
<p>By <%= post.author.name %></p>
<p>Category: <%= post.category.name %></p>
<p><%= post.excerpt %></p>
</div>
<% end %>

Every request re-renders every post, re-fetches every author and category, and rebuilds the same HTML that likely has not changed since the last request. Here is the same view rewritten with Russian Doll Caching applied to a collection render:

<%= render partial: "posts/post_card", collection: @posts, cached: true %>
<!-- _post_card.html.erb -->
<% cache(post) do %>
<div class="post-card">
<h2><%= post.title %></h2>
<p>By <%= post.author.name %></p>
<p>Category: <%= post.category.name %></p>
<p><%= post.excerpt %></p>
</div>
<% end %>

Now, only posts that have actually changed since the last render get regenerated. Everything else is served straight from the cache store, cutting both database queries and view rendering time significantly on a homepage that might otherwise load dozens of records on every visit.


When You Should Not Use Fragment Caching

Fragment caching is powerful, but it is not the right tool everywhere.

Dynamic dashboards. Analytics dashboards that show live, constantly changing numbers do not benefit much from fragment caching, since the cache would need to be invalidated almost as often as it is read.

Personalized pages. A page built entirely around one user's unique data, like an account settings page, has little to gain from caching since nothing about it is shared or repeated across requests.

Frequently changing data. If the underlying data changes every few seconds, your cache hit rate will be so low that the overhead of managing the cache outweighs any benefit.

Situations where caching may hurt rather than help. Adding caching to a low-traffic, cheap-to-render page adds complexity without a meaningful performance win, and it introduces one more place a bug can hide.


Conclusion

Fragment caching is one of the highest-value, lowest effort performance improvements available in Rails. It lets you skip expensive rendering work exactly where it matters, while leaving the rest of your application fully dynamic. Combined with Russian Doll Caching, a well-chosen cache store like Redis or Solid Cache, and cache keys tied closely to your data, you can meaningfully speed up pages that would otherwise slow down as your application grows.

Before reaching for fragment caching everywhere, take the time to profile your application and find out which views are actually expensive. Caching the right ten percent of your views often delivers most of the performance win, without adding unnecessary complexity to the other ninety percent.

If your Rails app has pages that feel sluggish, this is a great place to start. Pick one expensive view, wrap it in a cache block, watch your logs to confirm it is working, and measure the difference for yourself.

💌 Don’t miss out! Join my newsletter for web development tips, tutorials, and insights delivered straight to your inbox.

Thanks for reading & Happy coding! 🚀

Follow me on:

Code. Learn. Grow.

A friendly newsletter sharing dev tips, lessons, and wins from my journey.

    Services Tailored to Your Needs


    coding

    Web & Mobile Development

    Custom websites and mobile apps built to be fast, modern, and user-friendly. From sleek landing pages to full-scale applications, I deliver solutions that engage your audience and grow your business.

    API development

    Seamlessly connect your systems with secure, scalable APIs. I design and integrate APIs that improve efficiency, reliability, and flexibility for your business processes.

    Database design and management

    Reliable database solutions tailored to your needs. I design, optimize, and maintain databases that ensure performance, security, and scalability for your applications.

    You might also like…

    Ruby on Rails Beginner Guide: Free Github Roadmap
    Learning Concepts

    Ruby On Rails Beginner Guide: Free Github Roadmap

    By Jean Emmanuel Cadet
    Published on: Sep 02, 2025
    Rails link_to vs button_to: When Should You Use Each?
    Web Development

    Rails Link_to Vs Button_to: When Should You Use Each?

    By Jean Emmanuel Cadet
    Published on: May 23, 2026
    Fetch API vs Axios: Which Should You Use in 2026?
    Web Development

    Fetch API Vs Axios: Which Should You Use In 2026?

    By Jean Emmanuel Cadet
    Published on: Jun 27, 2026
    CodeCurious

    Designed for those who view software as architecture and code as literature.

    Legal

    Terms & Conditions Privacy Policy Disclaimer

    CodeCurious © 2025 - 2026. All rights reserved. | Made with ♥ by @jecode93