Cache in Rails: Types, Usage, and Best Practices

Cache in Rails: Types, Usage, and Best Practices

Caching in Rails: Boost Performance with Simple Techniques

🧠 What is Caching?

Simple Definition

Caching is storing expensive-to-compute results so you can reuse them later. Instead of doing the same work repeatedly, you save the result and reuse it when needed.

Real-World Example

Think of a restaurant kitchen:

  • Without Caching: Chef starts from scratch for every order
  • With Caching: Chef prepares popular ingredients in advance

In Web Applications

  • Database Queries: Store query results instead of hitting database repeatedly
  • View Rendering: Save rendered HTML instead of generating from scratch
  • API Responses: Cache external API calls for faster responses
  • Computed Results: Store expensive calculations for reuse

Why Caching Matters

  • Speed: 10-100x faster responses
  • Efficiency: Reduces CPU and database load
  • Scalability: Handle more users with same hardware
  • Cost: Lower hosting costs

What to Cache vs What NOT to Cache

✅ Cache These
Static content, product listings, blog posts, search results, analytics data, API responses
❌ Don’t Cache These
User-specific data, real-time data, sensitive information, frequently changing data
💡 Key Takeaway
Caching is about storing expensive-to-compute results so you can reuse them later. It’s like having a memory system for your application that makes everything faster and more efficient.

🚀 Rails Caching Basics

What Rails Provides

Rails comes with a powerful caching system built-in. You don’t need to install anything extra!

Cache Stores

Memory Store (Default)
Fast but lost when server restarts. Good for development.
Redis Store
Fast, persistent, and great for production. Recommended.
File Store
Persistent but slower than memory. Good for small apps.
Null Store
Disables caching. Useful for testing.

Basic Configuration

# Development
config.cache_store = :memory_store, { size: 64.megabytes }

# Production  
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }

Quick Examples

# In Controller
@products = Rails.cache.fetch("all_products", expires_in: 1.hour) do
  Product.all
end

# In View
<% cache @product do %>
  <div class="product">
    <h3><%= @product.name %></h3>
  </div>
<% end %>

Key Concepts

  • Cache Keys: cache @user becomes users/123-20231201120000
  • Expiration: expires_in: 1.hour – Cache expires after 1 hour
  • Manual Delete: Rails.cache.delete("key") – Delete specific cache
  • Clear All: Rails.cache.clear – Delete all cache
🎯 Quick Start
  • Enable caching: rails dev:cache
  • Use Redis for production
  • Start with Rails.cache.fetch
  • Use <% cache @object %> in views
  • Always set expiration times

🧩 Fragment Caching (Most Common)

What is Fragment Caching?

Fragment caching allows you to cache specific parts of your views, like individual product cards, user profiles, or blog post summaries.

Basic Syntax

<% cache @product do %>
  <div class="product-card">
    <h3><%= @product.name %></h3>
    <p><%= @product.description %></p>
    <span class="price"><%= @product.price %></span>
  </div>
<% end %>

Pros & Cons

✅ Pros
Granular control, automatic invalidation, easy implementation, 50-80% performance boost
❌ Cons
Cache key complexity, memory usage, debugging difficulty, cache stampede risk

Advanced Examples

1. Caching with Custom Keys


<% cache ["v1", "product", @product, current_user] do %>
  <div class="product-card">
    <h3><%= @product.name %></h3>
    <p><%= @product.description %></p>
    <% if current_user.admin? %>
      <div class="admin-controls">
        <%= link_to "Edit", edit_product_path(@product) %>
      </div>
    <% end %>
  </div>
<% end %>
        

2. Conditional Caching


<% cache_if @product.published?, @product do %>
  <div class="product-card">
    <h3><%= @product.name %></h3>
    <p><%= @product.description %></p>
  </div>
<% end %>

<% cache_unless @product.draft?, @product do %>
  <div class="product-card">
    <h3><%= @product.name %></h3>
    <p><%= @product.description %></p>
  </div>
<% end %>
        

3. Caching Collections


<% cache_collection @products do |product| %>
  <div class="product-card">
    <h3><%= product.name %></h3>
    <p><%= product.description %></p>
    <span class="price"><%= product.price %></span>
  </div>
<% end %>
        

4. Nested Fragment Caching (Russian Doll)


<% cache @category do %>
  <div class="category">
    <h2><%= @category.name %></h2>
    <div class="products">
      <% @category.products.each do |product| %>
        <% cache product do %>
          <div class="product-card">
            <h3><%= product.name %></h3>
            <p><%= product.description %></p>
            <% product.variants.each do |variant| %>
              <% cache variant do %>
                <div class="variant">
                  <span><%= variant.name %></span>
                  <span><%= variant.price %></span>
                </div>
              <% end %>
            <% end %>
          </div>
        <% end %>
      <% end %>
    </div>
  </div>
<% end %>
        

5. Caching with Expiration


<% cache @product, expires_in: 30.minutes do %>
  <div class="product-card">
    <h3><%= @product.name %></h3>
    <p><%= @product.description %></p>
  </div>
<% end %>
        
💡 Pro Tips
  • Use cache_collection for better performance with lists
  • Include user-specific data in cache keys for personalized content
  • Cache at the highest level possible to reduce cache key generation
  • Use cache_if and cache_unless for conditional caching
  • Monitor cache performance in production with tools like Redis Commander

🔧 Low-Level Caching

What is Low-Level Caching?

Low-level caching gives you direct control over what gets cached. Unlike fragment caching which caches HTML, low-level caching caches raw data – database queries, computed values, API responses, or any Ruby object.

Basic Syntax

# Basic usage
@user = Rails.cache.fetch("user_#{params[:id]}", expires_in: 1.hour) do
  User.find(params[:id])
end

# With default value
@products = Rails.cache.fetch("featured_products", expires_in: 30.minutes) do
  Product.where(featured: true).limit(10)
end

Pros & Cons

✅ Pros
Direct control, flexible keys, data caching, cross-request sharing, background processing, API responses
❌ Cons
Manual management, key collisions, memory usage, debugging complexity, serialization overhead

Common Use Cases

1. Database Queries

class Product < ApplicationRecord
  def self.featured_products
    Rails.cache.fetch("featured_products", expires_in: 1.hour) do
      where(featured: true).includes(:category, :reviews).to_a
    end
  end
end

2. Computed Values

class User < ApplicationRecord
  def total_purchases
    Rails.cache.fetch("user_#{id}_total_purchases", expires_in: 1.day) do
      orders.sum(:total_amount)
    end
  end
end

3. API Responses

class WeatherService
  def self.current_weather(city)
    Rails.cache.fetch("weather_#{city.downcase}", expires_in: 30.minutes) do
      response = HTTP.get("https://api.weather.com/#{city}")
      JSON.parse(response.body.to_s)
    end
  end
end

Cache Key Best Practices

Descriptive Keys
Use "user_#{user.id}_recent_orders" instead of "data"
Versioned Keys
Include version numbers for breaking changes: "v2_user_#{user.id}_profile"
Namespaced Keys
Use arrays: ["products", "featured", "limit_10"]
Time-Based Keys
Include timestamps for time-sensitive data: "daily_stats_#{Date.current}"

Cache Invalidation Strategies

1. Manual Invalidation

# Delete specific cache
Rails.cache.delete("user_#{user.id}_profile")

# Delete multiple related caches
Rails.cache.delete_matched("user_#{user.id}_*")

# Clear all cache (use carefully!)
Rails.cache.clear

2. Model Callbacks

class User < ApplicationRecord
  after_update :clear_user_cache
  
  private
  
  def clear_user_cache
    Rails.cache.delete_matched("user_#{id}_*")
  end
end

Best Practices

Cache Key Naming
Use descriptive names, include context, use namespaces
Expiration Strategy
5-15 min for dynamic, 1-6 hours for semi-static, 1 day+ for static
Performance
Only cache expensive operations, monitor memory usage, use background jobs
Invalidation
Use delete_matched for related caches, set up model callbacks
💡 Pro Tips
  • Use Rails.cache.fetch with a block for automatic cache generation
  • Include timestamps in cache keys for time-sensitive data
  • Use delete_matched to clear related caches efficiently
  • Monitor cache hit ratios to optimize expiration times
  • Consider using background jobs for expensive cache generation

🗝️ Cache Keys and Versioning

What are Cache Keys?

Cache keys are unique identifiers that tell Rails where to store and retrieve cached data. Think of them like file names in a filing cabinet - each piece of cached data needs a unique name to find it later.

How Rails Generates Cache Keys


# Model objects automatically generate keys
@user = User.find(123)
cache_key = @user.cache_key
# Result: "users/123-20231201120000"

# With version
cache_key_with_version = @user.cache_key_with_version
# Result: "users/123-20231201120000-1"
        

Cache Key Components

  • Model Name: The table name (e.g., "users", "products")
  • Record ID: The primary key of the record
  • Updated At: Timestamp of last update (for automatic invalidation)
  • Version: Optional version number for breaking changes

Custom Cache Keys


# Simple string key
Rails.cache.fetch("featured_products", expires_in: 1.hour) do
  Product.where(featured: true)
end

# Array-based key (recommended)
Rails.cache.fetch(["products", "featured", "limit_10"], expires_in: 1.hour) do
  Product.where(featured: true).limit(10)
end

# Complex key with multiple components
Rails.cache.fetch(["user", user.id, "orders", "recent", user.orders.maximum(:updated_at)]) do
  user.orders.recent
end
        

Cache Versioning Strategies

1. Model-Based Versioning


# Automatically includes model's updated_at timestamp
<% cache @user do %>
  <div><%= @user.name %></div>
<% end %>

# Cache key: "users/123-20231201120000"
        

2. Manual Versioning


# Version for breaking changes
<% cache ["v2", @user] do %>
  <div><%= @user.name %></div>
<% end %>

# Cache key: "v2/users/123-20231201120000"
        

3. Time-Based Versioning


# Daily versioning
Rails.cache.fetch(["daily_stats", Date.current.to_s], expires_in: 1.day) do
  generate_daily_statistics
end

# Hourly versioning
Rails.cache.fetch(["hourly_data", Time.current.beginning_of_hour.to_i], expires_in: 1.hour) do
  generate_hourly_data
end
        

Advanced Cache Key Patterns

1. Dependency-Based Keys


class Product < ApplicationRecord
  def self.products_by_category(category)
    # Include category's updated_at to invalidate when category changes
    cache_key = ["products", "category", category.id, category.updated_at.to_i]
    
    Rails.cache.fetch(cache_key, expires_in: 1.hour) do
      where(category: category).includes(:variants).to_a
    end
  end
end
        

2. User-Specific Keys


# Include user context for personalized content
Rails.cache.fetch(["user_dashboard", current_user.id, current_user.updated_at.to_i]) do
  {
    recent_orders: current_user.orders.recent,
    favorite_products: current_user.favorite_products,
    recommendations: current_user.recommendations
  }
end
        

3. Locale-Based Keys


# Include locale for internationalized content
Rails.cache.fetch(["product", @product, I18n.locale]) do
  {
    name: @product.name,
    description: @product.description,
    price: @product.price_in_locale(I18n.locale)
  }
end
        

Cache Key Best Practices

  • Use Arrays: cache ["products", "featured"] instead of cache "products_featured"
  • Include Dependencies: Add related model timestamps to invalidate when dependencies change
  • Be Descriptive: Use clear, meaningful key components
  • Version Breaking Changes: Include version numbers for schema changes
  • Consider Key Length: Very long keys can impact performance
💡 Pro Tips
  • Use cache_key_with_version for automatic versioning
  • Include updated_at timestamps for automatic invalidation
  • Use arrays for cache keys to avoid string concatenation
  • Version your cache keys when making breaking changes
  • Monitor cache key patterns to avoid collisions

🎪 Russian Doll Caching

What is Russian Doll Caching?

Russian Doll caching is a nested caching strategy where you cache fragments within other cached fragments. Like Russian nesting dolls, each cache contains smaller caches inside it. When an inner cache expires, it automatically invalidates all outer caches that depend on it.

How It Works

<% cache @category do %>                    # Outer cache (Category)
  <h2><%= @category.name %></h2>
  <div class="products">
    <% @category.products.each do |product| %>
      <% cache product do %>                # Inner cache (Product)
        <div class="product">
          <h3><%= product.name %></h3>
          <p><%= product.description %></p>
          <% product.variants.each do |variant| %>
            <% cache variant do %>          # Innermost cache (Variant)
              <div class="variant">
                <span><%= variant.name %></span>
                <span><%= variant.price %></span>
              </div>
            <% end %>
          <% end %>
        </div>
      <% end %>
    <% end %>
  </div>
<% end %>

Key Benefits

✅ Automatic Invalidation
When a product updates, all category and variant caches are automatically invalidated
✅ Granular Control
Cache at multiple levels for maximum performance
✅ Efficient Updates
Only changed data and its dependents are regenerated
❌ Complexity
Can become difficult to understand and debug

Best Practices

Use Model Objects
Let Rails generate cache keys automatically
Include Dependencies
Add related model timestamps to cache keys
Monitor Performance
Ensure benefits outweigh overhead
Test Invalidation
Test cache invalidation thoroughly in development
💡 Pro Tips
  • Start with simple caching and add Russian Doll patterns gradually
  • Use touch: true on associations to invalidate parent caches
  • Monitor cache performance to ensure benefits outweigh overhead
  • Test cache invalidation thoroughly in development

⚙️ Cache Store Configuration

What is a Cache Store?

A cache store is where Rails stores your cached data. Think of it as the "filing cabinet" for your cache. Rails supports multiple types of cache stores, each with different characteristics for speed, persistence, and memory usage.

Available Cache Stores

1. Memory Store (Default)


# config/environments/development.rb
config.cache_store = :memory_store, { size: 64.megabytes }

# Pros: Fastest, no external dependencies
# Cons: Lost on server restart, limited by RAM
        

2. File Store


# config/environments/development.rb
config.cache_store = :file_store, "/tmp/rails_cache"

# Pros: Persistent, no external dependencies
# Cons: Slower than memory, disk space usage
        

3. Redis Store (Recommended for Production)


# config/environments/production.rb
config.cache_store = :redis_cache_store, {
  url: ENV['REDIS_URL'],
  connect_timeout: 30,
  read_timeout: 0.2,
  write_timeout: 0.2,
  reconnect_attempts: 1,
  error_handler: -> (method:, returning:, exception:) {
    Rails.logger.error "Redis cache error: #{exception}"
  }
}

# Pros: Fast, persistent, scalable, feature-rich
# Cons: Requires Redis server, additional infrastructure
        

4. Memcached Store


# config/environments/production.rb
config.cache_store = :mem_cache_store, "localhost:11211"

# Pros: Fast, mature, battle-tested
# Cons: Older technology, less feature-rich than Redis
        

5. Null Store (Testing)


# config/environments/test.rb
config.cache_store = :null_store

# Pros: Disables caching for testing
# Cons: Not for production use
        

Redis Configuration (Production)

Basic Redis Setup


# Gemfile
gem 'redis', '~> 5.0'
gem 'hiredis-client', '~> 0.11'  # Optional: faster Redis client

# config/environments/production.rb
config.cache_store = :redis_cache_store, {
  url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1'),
  expires_in: 1.day,
  compress: true,
  compression_threshold: 1.kilobyte
}
        

Advanced Redis Configuration


# config/environments/production.rb
config.cache_store = :redis_cache_store, {
  url: ENV['REDIS_URL'],
  connect_timeout: 30,
  read_timeout: 0.2,
  write_timeout: 0.2,
  reconnect_attempts: 1,
  reconnect_delay: 0.3,
  reconnect_delay_max: 2.0,
  error_handler: -> (method:, returning:, exception:) {
    Rails.logger.error "Redis cache error: #{exception}"
    Sentry.capture_exception(exception) if defined?(Sentry)
  },
  expires_in: 1.day,
  compress: true,
  compression_threshold: 1.kilobyte,
  namespace: "myapp:#{Rails.env}"
}
        

Environment-Specific Configuration

Development Environment


# config/environments/development.rb
config.cache_store = :memory_store, { size: 64.megabytes }

# Enable caching in development
# Run: rails dev:cache
        

Test Environment


# config/environments/test.rb
config.cache_store = :null_store

# This disables caching for tests
# Ensures tests don't depend on cached data
        

Production Environment


# config/environments/production.rb
config.cache_store = :redis_cache_store, {
  url: ENV['REDIS_URL'],
  expires_in: 1.day,
  compress: true,
  namespace: "myapp:prod"
}

# Ensure Redis is running and accessible
# Monitor Redis memory usage
        

Cache Store Comparison

Store TypeSpeedPersistenceMemoryUse Case
MemoryFastestNoRAMDevelopment
FileSlowYesDiskSmall apps
RedisVery FastYesRAMProduction
MemcachedFastNoRAMLegacy apps

Best Practices

1. Environment Setup

  • Development: Use memory store with rails dev:cache
  • Testing: Use null store to avoid cache dependencies
  • Production: Use Redis for performance and persistence

2. Redis Configuration

  • Set Memory Limits: Configure Redis maxmemory and eviction policy
  • Enable Compression: Use compression for large cache entries
  • Use Namespaces: Prevent key collisions between environments

3. Monitoring and Maintenance

  • Monitor Memory Usage: Track Redis memory consumption
  • Set Up Alerts: Alert on cache store failures
  • Regular Cleanup: Clear old cache entries periodically
💡 Pro Tips
  • Use Redis for production - it's fast, persistent, and feature-rich
  • Enable compression for large cache entries to save memory
  • Use namespaces to separate cache between environments
  • Monitor Redis memory usage and set appropriate limits
  • Set up Redis clustering for high-availability applications

📝 View Caching (Template Digest-based)

What is View Caching?

View caching automatically generates cache keys based on template content and dependencies. Rails uses template digests (MD5 hashes of template content) to create unique cache keys that automatically invalidate when templates change.

How Template Digest Works

  • Automatic Key Generation: Rails generates cache keys based on template content and dependencies
  • Content-Based Invalidation: Cache automatically invalidates when template content changes
  • Dependency Tracking: Includes all partials and layouts in cache key generation
  • Built-in with Cache Helper: Works automatically with <% cache @object %>

Basic Usage

1. Simple View Caching


# app/views/products/show.html.erb
<% cache @product do %>
  <div class="product-detail">
    <h1><%= @product.name %></h1>
    <p><%= @product.description %></p>
    <span class="price"><%= @product.price %></span>
  </div>
<% end %>

# Generated cache key: "views/products/123-20231201120000-abc123def456"
# The last part is the template digest
        

2. Caching with Dependencies


# app/views/products/index.html.erb
<% cache ["products", "index", @products.maximum(:updated_at)] do %>
  <h1>All Products</h1>
  <div class="products-grid">
    <% @products.each do |product| %>
      <%= render partial: "product", locals: { product: product } %>
    <% end %>
  </div>
<% end %>

# app/views/products/_product.html.erb
<% cache product do %>
  <div class="product-card">
    <h3><%= product.name %></h3>
    <p><%= product.description %></p>
  </div>
<% end %>
        

3. Conditional View Caching


# app/views/posts/show.html.erb
<% cache_if @post.published?, @post do %>
  <article class="post">
    <h1><%= @post.title %></h1>
    <div class="content">
      <%= @post.content %>
    </div>
  </article>
<% end %>

# Only cache published posts
# app/views/posts/show.html.erb
<% cache_unless @post.draft?, @post do %>
  <article class="post">
    <h1><%= @post.title %></h1>
    <div class="content">
      <%= @post.content %>
    </div>
  </article>
<% end %>
        

Advanced View Caching

1. Collection Caching


# app/views/products/index.html.erb
<% cache_collection @products do |product| %>
  <div class="product-card">
    <h3><%= product.name %></h3>
    <p><%= product.description %></p>
    <span class="price"><%= product.price %></span>
  </div>
<% end %>

# This generates individual cache keys for each product
# More efficient than caching the entire collection
        

2. Nested View Caching (Russian Doll)


# app/views/categories/show.html.erb
<% cache @category do %>
  <div class="category">
    <h2><%= @category.name %></h2>
    <div class="products">
      <% @category.products.each do |product| %>
        <% cache product do %>
          <div class="product">
            <h3><%= product.name %></h3>
            <p><%= product.description %></p>
            <% product.variants.each do |variant| %>
              <% cache variant do %>
                <div class="variant">
                  <span><%= variant.name %></span>
                  <span><%= variant.price %></span>
                </div>
              <% end %>
            <% end %>
          </div>
        <% end %>
      <% end %>
    </div>
  </div>
<% end %>
        

3. Custom Cache Keys


# app/views/users/profile.html.erb
<% cache ["user", "profile", @user, @user.posts.maximum(:updated_at)] do %>
  <div class="user-profile">
    <h1><%= @user.name %></h1>
    <p><%= @user.bio %></p>
    <div class="recent-posts">
      <% @user.posts.recent.each do |post| %>
        <% cache post do %>
          <div class="post-summary">
            <h3><%= post.title %></h3>
            <p><%= post.excerpt %></p>
          </div>
        <% end %>
      <% end %>
    </div>
  </div>
<% end %>
        

Template Digest Configuration

1. Development Configuration


# config/environments/development.rb
config.action_view.cache_template_loading = true

# Enable template caching in development
# This helps catch template-related cache issues early
        

2. Production Configuration


# config/environments/production.rb
config.action_view.cache_template_loading = true

# Template digests are automatically generated and cached
# No additional configuration needed
        
💡 Pro Tips
  • Template digests automatically invalidate cache when templates change
  • Use cache_collection for better performance with lists
  • Include user-specific data in cache keys for personalized content
  • Cache at the highest level possible to reduce cache key generation
  • Use cache_if and cache_unless for conditional caching

🌐 HTTP Caching (Conditional GET)

What is HTTP Caching?

HTTP caching uses HTTP headers (ETag, Last-Modified, Cache-Control) to enable browser and proxy caching. It allows clients to avoid downloading unchanged content by sending conditional requests.

How HTTP Caching Works

  • ETag Headers: Unique identifiers for content that change when content changes
  • Last-Modified: Timestamp indicating when content was last updated
  • Conditional Requests: Clients send If-None-Match or If-Modified-Since headers
  • 304 Not Modified: Server responds with 304 when content hasn't changed
  • Cache-Control: Directives for how long content can be cached

Basic HTTP Caching

1. Using fresh_when


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    
    # Check if content is fresh
    fresh_when(@post)
  end
  
  def index
    @posts = Post.all
    
    # Use the most recent post's updated_at
    fresh_when(@posts.maximum(:updated_at))
  end
end
        

2. Using stale?


# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    
    # Check if content is stale
    if stale?(@product)
      respond_to do |format|
        format.html { render :show }
        format.json { render json: @product }
      end
    end
  end
  
  def index
    @products = Product.all
    last_modified = @products.maximum(:updated_at)
    
    if stale?(last_modified)
      respond_to do |format|
        format.html { render :index }
        format.json { render json: @products }
      end
    end
  end
end
        

3. Custom ETags


# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def profile
    @user = User.find(params[:id])
    
    # Custom ETag based on user and their posts
    etag = Digest::MD5.hexdigest("#{@user.cache_key}-#{@user.posts.maximum(:updated_at)}")
    
    fresh_when(etag: etag)
  end
  
  def dashboard
    @user = current_user
    @stats = @user.statistics
    
    # ETag based on user and stats
    etag = Digest::MD5.hexdigest("#{@user.cache_key}-#{@stats.updated_at}")
    
    if stale?(etag: etag)
      render :dashboard
    end
  end
end
        

Advanced HTTP Caching

1. Conditional Caching


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    
    # Only cache for public posts
    if @post.published?
      fresh_when(@post)
    else
      # Don't cache private posts
      response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
      response.headers['Pragma'] = 'no-cache'
      response.headers['Expires'] = '0'
    end
  end
  
  def index
    @posts = Post.published
    
    # Cache with custom logic
    if current_user&.admin?
      # Admins see different content, don't cache
      response.headers['Cache-Control'] = 'private, no-cache'
    else
      # Public users can cache
      fresh_when(@posts.maximum(:updated_at))
    end
  end
end
        

2. API Caching


# app/controllers/api/v1/products_controller.rb
class Api::V1::ProductsController < ApplicationController
  def index
    @products = Product.all
    
    # Cache API responses
    fresh_when(@products.maximum(:updated_at))
  end
  
  def show
    @product = Product.find(params[:id])
    
    # Custom ETag for API
    etag = Digest::MD5.hexdigest("#{@product.cache_key}-#{@product.reviews.maximum(:updated_at)}")
    
    if stale?(etag: etag)
      render json: @product.as_json(include: :reviews)
    end
  end
  
  def search
    query = params[:q]
    @products = Product.search(query)
    
    # Cache search results
    etag = Digest::MD5.hexdigest("#{query}-#{@products.maximum(:updated_at)}")
    
    if stale?(etag: etag)
      render json: @products
    end
  end
end
        

3. Complex ETag Generation


# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  def index
    @user = current_user
    @stats = generate_user_stats(@user)
    @recent_activity = @user.recent_activity
    
    # Complex ETag combining multiple data sources
    etag_components = [
      @user.cache_key,
      @stats.updated_at,
      @recent_activity.maximum(:updated_at),
      @user.preferences.updated_at
    ]
    
    etag = Digest::MD5.hexdigest(etag_components.join('-'))
    
    if stale?(etag: etag)
      render :index
    end
  end
  
  private
  
  def generate_user_stats(user)
    # Expensive operation that we want to cache
    {
      total_posts: user.posts.count,
      total_likes: user.posts.sum(:likes_count),
      average_rating: user.posts.average(:rating)
    }
  end
end
        

Cache Control Headers

1. Setting Cache Headers


# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_default_cache_headers
  
  private
  
  def set_default_cache_headers
    # Set default cache headers
    response.headers['Cache-Control'] = 'public, max-age=300' # 5 minutes
  end
  
  def set_cache_headers(duration = 1.hour)
    response.headers['Cache-Control'] = "public, max-age=#{duration.to_i}"
  end
  
  def set_private_cache_headers(duration = 1.hour)
    response.headers['Cache-Control'] = "private, max-age=#{duration.to_i}"
  end
  
  def disable_caching
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
  end
end
        

2. Conditional Cache Headers


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    
    if @post.published?
      # Public posts can be cached by browsers and CDNs
      set_cache_headers(1.hour)
      fresh_when(@post)
    else
      # Private posts should not be cached
      disable_caching
      render :show
    end
  end
  
  def index
    @posts = Post.published
    
    if current_user&.admin?
      # Admins see different content
      set_private_cache_headers(5.minutes)
    else
      # Public users can cache longer
      set_cache_headers(1.hour)
    end
    
    fresh_when(@posts.maximum(:updated_at))
  end
end
        
💡 Pro Tips
  • Use fresh_when for simple cases, stale? for complex logic
  • Include all relevant data sources in ETag generation
  • Set appropriate Cache-Control headers for your use case
  • Test HTTP caching with tools like curl or browser dev tools
  • Monitor 304 responses to measure cache effectiveness

🔒 Cache Security & Best Practices

Security Considerations

Caching can introduce security vulnerabilities if not implemented carefully. Understanding these risks and implementing proper safeguards is crucial for production applications.

Common Security Risks

1. Cache Poisoning


# VULNERABLE: User input in cache key
Rails.cache.fetch("user_data_#{params[:user_id]}") do
  User.find(params[:user_id])
end

# SECURE: Validate and sanitize input
user_id = params[:user_id].to_i
if user_id > 0
  Rails.cache.fetch("user_data_#{user_id}") do
    User.find(user_id)
  end
end
        

2. Information Disclosure


# VULNERABLE: Caching sensitive data
Rails.cache.fetch("user_#{user.id}") do
  {
    email: user.email,
    password_hash: user.password_digest,  # Never cache this!
    credit_card: user.credit_card_number  # Never cache this!
  }
end

# SECURE: Only cache non-sensitive data
Rails.cache.fetch("user_#{user.id}") do
  {
    name: user.name,
    avatar_url: user.avatar_url,
    preferences: user.preferences
  }
end
        

3. Cache Key Collisions


# VULNERABLE: Generic cache keys
Rails.cache.fetch("data") do
  expensive_operation
end

# SECURE: Specific, namespaced keys
Rails.cache.fetch(["users", "profile", user.id, user.updated_at.to_i]) do
  expensive_operation
end
        

Security Best Practices

1. Input Validation


class CacheService
  def self.safe_cache_key(prefix, *components)
    # Validate and sanitize all components
    safe_components = components.map do |component|
      case component
      when Integer
        component.to_s
      when String
        component.gsub(/[^a-zA-Z0-9_-]/, '_')
      when ActiveRecord::Base
        "#{component.class.name.downcase}_#{component.id}"
      else
        component.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')
      end
    end
    
    [prefix, *safe_components].join('/')
  end
end

# Usage
cache_key = CacheService.safe_cache_key("products", category_id, user_id)
Rails.cache.fetch(cache_key) do
  Product.where(category_id: category_id)
end
        

2. User-Specific Caching


# Always include user context for personalized data
Rails.cache.fetch(["user", current_user.id, "dashboard", Date.current]) do
  {
    recent_orders: current_user.orders.recent,
    recommendations: current_user.recommendations,
    notifications: current_user.notifications.unread
  }
end

# For shared data, be explicit about what's shared
Rails.cache.fetch(["public", "products", "featured"]) do
  Product.where(featured: true).limit(10)
end
        

3. Sensitive Data Protection


class User < ApplicationRecord
  # Never cache these attributes
  NEVER_CACHE_ATTRIBUTES = %w[
    password_digest
    reset_password_token
    confirmation_token
    credit_card_number
    ssn
  ].freeze
  
  def cacheable_attributes
    attributes.except(*NEVER_CACHE_ATTRIBUTES)
  end
  
  def cache_key
    # Include only safe attributes in cache key
    "users/#{id}-#{updated_at.to_i}"
  end
end
        

Monitoring and Debugging

1. Cache Hit Monitoring


class CacheMonitor
  def self.track_hit_rate(cache_key)
    hit_count = Rails.cache.read("hit_count_#{cache_key}") || 0
    miss_count = Rails.cache.read("miss_count_#{cache_key}") || 0
    
    if Rails.cache.exist?(cache_key)
      hit_count += 1
      Rails.cache.write("hit_count_#{cache_key}", hit_count)
    else
      miss_count += 1
      Rails.cache.write("miss_count_#{cache_key}", miss_count)
    end
    
    total = hit_count + miss_count
    hit_rate = total > 0 ? (hit_count.to_f / total * 100).round(2) : 0
    
    Rails.logger.info "Cache hit rate for #{cache_key}: #{hit_rate}%"
  end
end
        

2. Cache Debugging


# Enable cache logging in development
# config/environments/development.rb
config.log_level = :debug

# Custom cache logger
class CacheLogger
  def self.log_cache_operation(operation, key, result = nil)
    Rails.logger.debug "Cache #{operation}: #{key} #{result ? "-> #{result}" : ""}"
  end
end

# Usage in your code
Rails.cache.fetch("test_key") do
  CacheLogger.log_cache_operation("MISS", "test_key")
  "cached_value"
end
        
🔒 Security Checklist
  • ✅ Never cache passwords, tokens, or sensitive personal data
  • ✅ Validate and sanitize all cache key inputs
  • ✅ Use user-specific cache keys for personalized content
  • ✅ Set appropriate expiration times for all cache entries
  • ✅ Monitor cache memory usage and hit rates
  • ✅ Use namespaces to prevent key collisions
  • ✅ Implement cache warming for critical data

🚀 Production Deployment

Pre-Deployment Checklist

  • Cache Store Selection: Choose Redis for production (fast, persistent, scalable)
  • Memory Configuration: Set appropriate Redis memory limits and eviction policies
  • Monitoring Setup: Configure cache monitoring and alerting
  • Security Review: Ensure no sensitive data is being cached
  • Performance Testing: Test cache performance under load

Redis Production Setup

1. Redis Configuration


# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000

# Enable persistence
appendonly yes
appendfsync everysec
        

2. Rails Configuration


# config/environments/production.rb
config.cache_store = :redis_cache_store, {
  url: ENV['REDIS_URL'],
  connect_timeout: 30,
  read_timeout: 0.2,
  write_timeout: 0.2,
  reconnect_attempts: 1,
  reconnect_delay: 0.3,
  reconnect_delay_max: 2.0,
  error_handler: -> (method:, returning:, exception:) {
    Rails.logger.error "Redis cache error: #{exception}"
    Sentry.capture_exception(exception) if defined?(Sentry)
  },
  expires_in: 1.day,
  compress: true,
  compression_threshold: 1.kilobyte,
  namespace: "myapp:#{Rails.env}"
}
        

Monitoring and Alerting

1. Cache Performance Monitoring


class CacheMetrics
  def self.collect_metrics
    redis = Redis.new(url: ENV['REDIS_URL'])
    
    {
      memory_usage: redis.info['used_memory_human'],
      hit_rate: calculate_hit_rate,
      cache_size: redis.dbsize,
      memory_fragmentation: redis.info['mem_fragmentation_ratio']
    }
  end
  
  def self.calculate_hit_rate
    # Implement hit rate calculation
    # This would track cache hits vs misses
  end
end

# Schedule metrics collection
# config/application.rb
config.after_initialize do
  if Rails.env.production?
    Thread.new do
      loop do
        metrics = CacheMetrics.collect_metrics
        Rails.logger.info "Cache metrics: #{metrics}"
        sleep 300 # Every 5 minutes
      end
    end
  end
end
        

2. Health Checks


class CacheHealthCheck
  def self.healthy?
    begin
      Rails.cache.write("health_check", "ok", expires_in: 1.minute)
      Rails.cache.read("health_check") == "ok"
    rescue => e
      Rails.logger.error "Cache health check failed: #{e.message}"
      false
    end
  end
  
  def self.memory_usage
    redis = Redis.new(url: ENV['REDIS_URL'])
    redis.info['used_memory_human']
  end
end

# Use in health check endpoint
# config/routes.rb
get '/health', to: 'health#check'

# app/controllers/health_controller.rb
class HealthController < ApplicationController
  def check
    cache_healthy = CacheHealthCheck.healthy?
    cache_memory = CacheHealthCheck.memory_usage
    
    render json: {
      status: cache_healthy ? 'healthy' : 'unhealthy',
      cache: {
        healthy: cache_healthy,
        memory_usage: cache_memory
      }
    }
  end
end
        

Deployment Strategies

1. Blue-Green Deployment


# Clear cache during deployment
# config/deploy.rb (Capistrano)
namespace :deploy do
  task :clear_cache do
    on roles(:app) do
      within release_path do
        execute :bundle, "exec rails runner 'Rails.cache.clear'"
      end
    end
  end
end

after 'deploy:updated', 'deploy:clear_cache'
        

2. Cache Warming


class ProductionCacheWarmingJob < ApplicationJob
  queue_as :default
  
  def perform
    Rails.logger.info "Starting production cache warming..."
    
    # Warm up critical caches
    warm_product_caches
    warm_user_caches
    warm_analytics_caches
    
    Rails.logger.info "Production cache warming completed"
  end
  
  private
  
  def warm_product_caches
    Product.featured_products
    Category.with_products
    Product.top_sellers
  end
  
  def warm_user_caches
    User.top_customers
    User.recent_activity
  end
  
  def warm_analytics_caches
    AnalyticsService.daily_sales_report
    AnalyticsService.user_purchase_patterns
  end
end

# Schedule after deployment
# config/application.rb
config.after_initialize do
  if Rails.env.production?
    ProductionCacheWarmingJob.perform_later
  end
end
        

Troubleshooting Production Issues

1. High Memory Usage

  • Check Large Keys: Use redis-cli --bigkeys to find large cache entries
  • Review Expiration: Ensure all cache entries have appropriate expiration times
  • Enable Compression: Use compression for large cache entries

2. Cache Misses

  • Monitor Hit Rates: Track cache hit/miss ratios
  • Review Cache Keys: Ensure cache keys are consistent
  • Check Expiration: Verify expiration times are appropriate
🚀 Production Checklist
  • ✅ Use Redis for production cache store
  • ✅ Configure Redis memory limits and eviction policies
  • ✅ Set up monitoring and alerting for cache performance
  • ✅ Implement cache warming after deployments
  • ✅ Configure health checks for cache availability
  • ✅ Set up proper error handling and logging
  • ✅ Test cache performance under load

🧪 Testing Caching

Testing Strategy

Testing caching requires special consideration because cached data can make tests unpredictable and slow. Rails provides tools to help you test caching effectively.

Test Environment Setup

1. Disable Caching in Tests


# config/environments/test.rb
config.cache_store = :null_store

# This ensures tests don't depend on cached data
# Each test runs with a clean slate
        

2. Enable Caching for Specific Tests


# spec/rails_helper.rb or test/test_helper.rb
RSpec.configure do |config|
  config.around(:each, :caching) do |example|
    # Temporarily enable caching for specific tests
    old_cache_store = Rails.cache
    Rails.cache = ActiveSupport::Cache::MemoryStore.new
    
    example.run
    
    Rails.cache = old_cache_store
  end
end
        

Testing Cache Behavior

1. Testing Cache Hit/Miss


# spec/models/product_spec.rb
RSpec.describe Product, type: :model do
  describe '.featured_products' do
    it 'caches the result', :caching do
      product = Product.create!(featured: true)
      
      # First call should cache
      expect(Rails.cache).to receive(:fetch).with("featured_products", expires_in: 1.hour)
      Product.featured_products
      
      # Second call should use cache
      expect(Rails.cache).to receive(:read).with("featured_products")
      Product.featured_products
    end
  end
end
        

2. Testing Cache Invalidation


# spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe 'cache invalidation' do
    it 'invalidates cache when user is updated', :caching do
      user = User.create!(name: 'John')
      
      # Cache user data
      Rails.cache.fetch("user_#{user.id}") { user.attributes }
      
      # Update user
      user.update!(name: 'Jane')
      
      # Cache should be invalidated
      expect(Rails.cache.exist?("user_#{user.id}")).to be false
    end
  end
end
        

3. Testing Fragment Caching


# spec/views/products/index_spec.rb
RSpec.describe 'products/index', type: :view do
  it 'caches product fragments', :caching do
    product = Product.create!(name: 'Test Product')
    assign(:products, [product])
    
    # Render the view
    render
    
    # Check that cache was used
    expect(Rails.cache.exist?("views/products/#{product.id}")).to be true
  end
end
        

Integration Testing

1. Testing Cache Performance


# spec/requests/products_spec.rb
RSpec.describe 'Products API', type: :request do
  describe 'GET /products' do
    it 'responds faster on subsequent requests', :caching do
      # Create test data
      Product.create!(name: 'Product 1', featured: true)
      Product.create!(name: 'Product 2', featured: true)
      
      # First request (cache miss)
      start_time = Time.current
      get '/products'
      first_request_time = Time.current - start_time
      
      # Second request (cache hit)
      start_time = Time.current
      get '/products'
      second_request_time = Time.current - start_time
      
      # Second request should be faster
      expect(second_request_time).to be < first_request_time
    end
  end
end
        

2. Testing Cache Warming


# spec/jobs/cache_warming_job_spec.rb
RSpec.describe CacheWarmingJob, type: :job do
  it 'warms up critical caches', :caching do
    # Create test data
    Product.create!(featured: true)
    User.create!(name: 'Test User')
    
    # Run the job
    CacheWarmingJob.perform_now
    
    # Verify caches were created
    expect(Rails.cache.exist?("featured_products")).to be true
    expect(Rails.cache.exist?("top_users")).to be true
  end
end
        

🎯 Interview Preparation

Common Interview Questions

1. Basic Concepts

  • Q: What is caching and why is it important?
    A: Caching stores expensive-to-compute results for reuse, improving performance by reducing database queries, API calls, and computation time.
  • Q: What are the different types of caching in Rails?
    A: Fragment caching (views), low-level caching (data), Russian Doll caching (nested), and cache stores (Redis, Memcached, Memory).
  • Q: How does Rails generate cache keys?
    A: Rails uses model name, ID, and updated_at timestamp: "users/123-20231201120000"

2. Implementation Questions

  • Q: How would you cache a list of products?
    A: Use fragment caching in views or low-level caching in models with appropriate expiration and cache keys.
  • Q: How do you handle cache invalidation?
    A: Use model callbacks, manual deletion, versioning, or time-based expiration depending on the use case.
  • Q: What cache store would you use in production?
    A: Redis - it's fast, persistent, scalable, and feature-rich compared to alternatives.

3. Advanced Questions

  • Q: How would you implement Russian Doll caching?
    A: Nest cache blocks with model objects, ensuring automatic invalidation when inner caches change.
  • Q: How do you monitor cache performance?
    A: Track hit rates, memory usage, response times, and use tools like Redis Commander or custom metrics.
  • Q: What are cache stampedes and how do you prevent them?
    A: Multiple requests generating the same cache simultaneously. Prevent with background jobs, locks, or staggered expiration.

System Design Questions

1. E-commerce Caching Strategy


# Design a caching strategy for an e-commerce site

# Product Catalog
- Cache product listings by category
- Cache individual product details
- Cache product search results
- Use Russian Doll caching for nested data

# User Data
- Cache user profiles (non-sensitive data only)
- Cache user preferences and settings
- Cache user order history

# Analytics
- Cache daily/weekly sales reports
- Cache top-selling products
- Cache user purchase patterns

# Implementation
class Product < ApplicationRecord
  def self.cached_by_category(category)
    Rails.cache.fetch(["products", "category", category.id, category.updated_at]) do
      where(category: category).includes(:variants).to_a
    end
  end
end
        

2. Social Media Feed Caching


# Design caching for a social media feed

# Feed Generation
- Cache user feeds with personalized content
- Cache trending posts and hashtags
- Cache user relationships and follows

# Content Caching
- Cache post content and metadata
- Cache user profiles and avatars
- Cache comment threads

# Real-time Considerations
- Use shorter cache times for dynamic content
- Implement cache warming for popular feeds
- Consider WebSocket updates for real-time features

# Implementation
class FeedService
  def self.user_feed(user_id, page = 1)
    Rails.cache.fetch(["feed", user_id, page, Date.current], expires_in: 15.minutes) do
      generate_user_feed(user_id, page)
    end
  end
end
        

Performance Optimization Questions

1. Cache Key Optimization

  • Q: How would you optimize cache keys?
    A: Use arrays instead of strings, include dependencies, keep keys short, and use namespaces.
  • Q: How do you handle cache memory usage?
    A: Set appropriate expiration times, use compression, monitor memory usage, and implement eviction policies.
  • Q: How would you implement cache warming?
    A: Use background jobs to pre-populate frequently accessed cache entries after deployment or data changes.

2. Scalability Questions

  • Q: How would you scale caching for high traffic?
    A: Use Redis clustering, implement cache warming, optimize cache keys, and monitor performance metrics.
  • Q: How do you handle cache failures?
    A: Implement fallback mechanisms, use circuit breakers, and ensure graceful degradation when cache is unavailable.

Practical Coding Questions

1. Cache Implementation


# Implement a caching service for user recommendations

class RecommendationService
  def self.user_recommendations(user_id)
    Rails.cache.fetch(["recommendations", user_id], expires_in: 1.hour) do
      user = User.find(user_id)
      
      {
        products: user.based_on_purchase_history,
        categories: user.favorite_categories,
        trending: Product.trending_in_user_category(user),
        personalized: generate_personalized_recommendations(user)
      }
    end
  end
  
  private
  
  def self.generate_personalized_recommendations(user)
    # Complex recommendation algorithm
    # This is expensive, so we cache the result
  end
end
        

2. Cache Invalidation


# Implement cache invalidation for a blog system

class BlogPost < ApplicationRecord
  after_update :invalidate_cache
  after_destroy :invalidate_cache
  
  def self.featured_posts
    Rails.cache.fetch("featured_posts", expires_in: 1.hour) do
      where(featured: true).includes(:author, :comments).to_a
    end
  end
  
  private
  
  def invalidate_cache
    Rails.cache.delete("featured_posts")
    Rails.cache.delete_matched("blog_post_#{id}_*")
    Rails.cache.delete_matched("author_#{author_id}_posts_*")
  end
end
        
🎯 Interview Tips
  • ✅ Understand the fundamentals: what, why, and how of caching
  • ✅ Be able to implement basic caching patterns
  • ✅ Know the trade-offs between different cache stores
  • ✅ Understand cache invalidation strategies
  • ✅ Be prepared to discuss performance optimization
  • ✅ Practice system design questions with caching
  • ✅ Know how to monitor and debug cache issues

🧪 Alternatives

  • CDNs for full page caching (e.g., Cloudflare)
  • Client-side caching with ETags or localStorage
  • Reverse proxy caching (e.g., Varnish, NGINX)
  • Memoization in code for one-time calculations

📦 Real World Use Case

Shopify-style Storefront:

Imagine a storefront that displays a product catalog with thousands of items. Each product has an image, pricing, and nested variants. Instead of re-rendering everything on every request, you can:

  • Use Rails.cache.fetch for product queries
  • Use <% cache product do %> in partials
  • Use Russian Doll caching for nested variants
  • Invalidate cache when product updates

This reduces database hits and makes the site handle large traffic easily.

Learn more about Rails

57 thoughts on “Cache in Rails: Types, Usage, and Best Practices”

Comments are closed.

Scroll to Top