Ruby on Rails Caching: Understand the different types of caching

khan_mansoor

Mansoor Khan

about 10 minutes Mar 12, 2024

Caching is essential for enhancing the performance and scalability of web applications — and caching in Ruby on Rails is no exception. By storing and reusing the results of expensive computations or database queries, caching significantly reduces the time and resources required to serve user requests.

Here, we review how to implement different types of caching in Rails, such as fragment caching and Russian doll caching. We also show you how to manage cache dependencies and choose cache stores and outline best practices for using caching effectively in a Rails application.

This article assumes you’re familiar with Ruby on Rails, use Rails version 6 or higher, and feel comfortable using Rails views. The code examples demonstrate how to use caching inside new or existing view templates.

Types of Ruby on Rails Caching

Several types of caching are available in Ruby on Rails applications, depending on the level and granularity of the content to cache. The primary types used in modern Rails apps are:

  • Fragment caching: Caches parts of a web page that don’t change frequently, such as headers, footers, sidebars, or static content. Fragment caching reduces the number of partials or components rendered on each request.
  • Russian doll caching: Caches nested fragments of a web page that depend on each other, such as collections and associations. Russian doll caching prevents unnecessary database queries and makes it easy to reuse unchanged cached fragments.
Two additional types of caching were previously part of Ruby on Rails but are now available as separate gems:

  • Page caching: Caches entire web pages as static files on the server, bypassing the entire page rendering lifecycle
  • Action caching: Caches the output of entire controller actions. It’s similar to page caching but allows you to apply filters like authentication.
Page and action caching are infrequently used and no longer recommended for most use cases in modern Rails apps.

Fragment Caching in Ruby on Rails

Fragment caching lets you cache parts of a page that change infrequently. For example, a page displaying a list of products with their associated prices and ratings could cache details that are unlikely to change.

Meanwhile, it could let Rails re-render dynamic parts of the page — like comments or reviews — on every page load. Fragment caching is less useful when a view’s underlying data changes frequently due to the overhead of frequently updating the cache.

As the simplest type of caching included in Rails, fragment caching should be your first choice when adding caching to your app to improve performance.

To use fragment caching in Rails, use the cache helper method in your views. For example, write the following code to cache a product partial in your view:

<% @products.each do |product| %>
  <% cache product do %>
    <%= render partial: "product", locals: { product: product } %>
  <% end %>
<% end %>
The cache helper generates a cache key based on each element’s class name, id, and updated_at timestamp (for example, products/1-20230501000000). The next time a user requests the same product, the cache helper will fetch the cached fragment from the cache store and display it without reading the product from the database.

You can also customize the cache key by passing options to the cache helper. For example, to include a version number or a timestamp in your cache key, write something like this:

<% @products.each do |product| %>
  <% cache [product, "v1"] do %>
    <%= render partial: "product", locals: { product: product } %>
  <% end %>
<% end %>
Alternatively, you can set an expiry time:

<% @products.each do |product| %>
  <% cache product, expires_in: 1.hour do %>
    <%= render partial: "product", locals: { product: product } %>
  <% end %>
<% end %>
The first example will append v1 to the cache key (for example, products/1-v1). This is useful for invalidating the cache when you change the partial template or layout. The second example sets an expiration time for the cache entry (1 hour), which helps expire stale data.

Russian Doll Caching in Ruby on Rails

Russian doll caching is a powerful caching strategy in Ruby on Rails that optimizes your application’s performance by nesting caches inside one another. It uses the Rails fragment caching and cache dependencies to minimize redundant work and improve load times.

In a typical Rails application, you often render a collection of items, each with multiple child components. When updating a single item, avoid re-rendering the entire collection or any unaffected items. Use Russian Doll caching when dealing with hierarchical or nested data structures, especially when the nested components have their own associated data that could change independently.

The downside of Russian Doll caching is that it adds complexity. You must understand the relationships between the nested levels of items you’re caching to ensure that you cache the right items. In some cases, you’ll need to add associations to your Active Record models so that Rails can infer the relationships between cached data items.

As with regular fragment caching, Russian doll caching uses the cache helper method. For example, to cache a category with its subcategories and products in your view, write something like this:

<% @categories.each do |category| %>
  <% cache category do %>
    <h2><%= category.name %></h2>
    <% category.subcategories.each do |subcategory| %>
    <% cache subcategory do %>
    <h3><%= subcategory.name %></h3>
    <% subcategory.products.each do |product| %>
    <% cache product do %>
        <%= render partial: "product", locals: { product: product } %>
        <% end %>
    <% end %>
    <% end %>
    <% end %>
  <% end %>
<% end %>
The cache helper will store each nested level separately in the cache store. The next time the same category is requested, it will fetch its cached fragment from the cache store and display it without rendering it again.

However, if any subcategory or product’s details change — like its name or description — this invalidates its cached fragment, which is then re-rendered with updated data. Russian doll caching ensures you don’t have to invalidate an entire category if a single subcategory or product changes.


Cache Dependency Management in Ruby on Rails

Cache dependencies are relationships between cached data and its underlying sources, and managing them can be tricky. If the source data changes, any associated cached data should expire.

Rails can use timestamps to manage most cache dependencies automatically. Every Active Record model has created_at and updated_at attributes indicating when the cache created or last updated the record. To ensure Rails can automatically manage caching, define your Active Record models’ relationships as follows:

class Product < ApplicationRecord
  belongs_to :category
end
class Category < ApplicationRecord
  has_many :products
end
In this example:

  • If you update a product record (for instance, by changing its price), its updated_at timestamp changes automatically.
  • If you use this timestamp as part of your cache key (like products/1-20230504000000), it also automatically invalidates your cached fragment.
  • To invalidate your category’s cached fragment when you update a product record — maybe because it shows some aggregated data like average price — use the touch method in your controller (@product.category.touch) or add a touch option in your model association (belongs_to :category touch: true).
Another mechanism for managing cache dependencies is using low-level caching methods — such as fetch and write — directly in your models or controllers. These methods allow you to store arbitrary data or content in your cache store with custom keys and options. For example:

class Product < ApplicationRecord
  def self.average_price
    Rails.cache.fetch("products/average_price", expires_in: 1.hour) do
    average(:price)
    end
  end
end
This example demonstrates how to cache calculated data — such as the average price of all products — for an hour using the fetch method with a custom key (products/average_price) and an expiration option (expires_in: 1.hour).

The fetch method will try to read the data from the cache store first. If it cannot find the data or the data’s expired, it executes the block and stores the result in the cache store.

To manually invalidate a cache entry before its expiry time, use the write method with the force option:

Rails.cache.write("products/average_price", Product.average(:price), force: true))

Cache Stores and Backends in Ruby on Rails

Rails lets you choose different cache stores or backends to store your cached data and content. The Rails cache store is an abstraction layer providing a common interface to interact with different storage systems. A cache backend implements the cache store interface for a specific storage system.

Rails supports several types of cache stores or backends out of the box, which are detailed below.

Memory Store

Memory store uses an in-memory hash as cache storage. It’s fast and simple but has limited capacity and persistence. This cache store is suitable for development and testing environments or small, simple applications.

Disk Store

Disk store uses files on the disk as cache storage. It’s the slowest caching option in Rails but has a large capacity and persistence. Disk store is suitable for applications that must cache large amounts of data and don’t need maximum performance.

Redis

The Redis store uses a Redis instance for cache storage. Redis is an in-memory data store that supports several data types. Although it’s fast and flexible, it requires a separate server and configuration. It’s suitable for applications that must cache complex or dynamic data that changes frequently. Redis is an ideal choice when running Rails apps in the cloud because some hosting providers, including Kinsta, offer Redis as a persistent object cache.

Memcached

The Memcached store uses a Memcached instance for cache storage. Memcached is an in-memory key-value store that supports simple data types and features. It’s fast and scalable, but like Redis, it requires a separate server and configuration. This store is suitable for applications that need to cache simple or static data that undergoes frequent updates.

You can configure your cache store in your Rails environments files (for example, config/environments/development.rb) using the config.cache_store option. Here’s how to use each of Rails’ built-in caching methods:

# Use memory store
config.cache_store = :memory_store
# Use disk store
config.cache_store = :file_store, "tmp/cache"
# Use Redis
config.cache_store = :redis_cache_store, { url: "redis://localhost:6379/0" }
# Use Memcached
config.cache_store = :mem_cache_store, "localhost"
You should only have one `config.cache_store` call per environment file. If you have more than one, the cache store only uses the last one.

Each cache store has unique advantages and disadvantages depending on your application’s needs and preferences. Choose the one that best suits your use case and experience level.


Best Practices for Ruby on Rails Caching

Using caching in your Rails application can significantly boost its performance and scalability, especially when you implement the following best practices:

  • Cache selectively: Cache only frequently accessed, expensive-to-generate, or infrequently updated data. Avoid over-caching to prevent excessive memory usage, stale data risks, and performance degradation.
  • Expire cache entries: Prevent stale data by expiring invalid or irrelevant entries. Use timestamps, expiration options, or manual invalidation.
  • Optimize cache performance: Choose the cache store that fits your application’s needs, and fine-tune its parameters — like size, compression, or serialization — for optimal performance.
  • Monitor and test cache impact: Evaluate cache behavior — like hit rate, miss rate, and latency — and assess their respective impacts on performance (response time, throughput, resource usage). Use tools like New Relic, Rails logs, ActiveSupport notifications, or Rack mini profiler.
Summary

Ruby on Rails caching enhances application performance and scalability by efficiently storing and reusing frequently accessed data or content. With a deeper understanding of caching techniques, you’re better equipped to deliver faster Rails apps to your users.

Source: originally posted here