thinkoodle/rails-skills

caching

Expert guidance for Rails caching — fragment caching, Russian doll caching, cache keys/versioning, low-level caching (Rails.cache), conditional GET (stale?/fresh_when), and cache stores (Solid Cache, Redis, Memcached).

First seen May 1, 2026

Installation

$ npx skills add thinkoodle/rails-skills --skill caching

Summary

  • Expert guidance for Rails caching — fragment caching, Russian doll caching, cache keys/versioning, low-level caching (Rails.cache), conditional GET (stale?/fresh_when), and cache stores (Solid Cache, Redis, Memcached).
  • Use when implementing cache, caching, fragment cache, Russian doll, Rails.cache, Solid Cache, cache key, HTTP caching, stale?, fresh_when, cache store, or optimizing performance.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from thinkoodle/rails-skills · top by installs.

npx skills add thinkoodle/rails-skills

Browse all from thinkoodle/rails-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 5
License MIT
Default branch master
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsRead, Grep, Glob, Write, Edit, Bash(bin/rails dev:cache), Bash(bin/rails runner *)

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 14,142 B
  • docs SUMMARY.md 414 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 12 installs

SKILL.md

Rails Caching Expert

Implement fast, correct caching in Rails applications. Prefer cache correctness over cache coverage — stale data is worse than slow data.

Philosophy

Core Principles:

  1. Cache correctness first — A cache miss is slow; stale data is a bug
  2. Invalidation is the hard part — Every cache you add is a cache you must invalidate
  3. Measure before caching — Don't cache what isn't slow
  4. Solid Cache is the Rails 8 default — Use it unless you have a specific reason not to
  5. Fragment caching is your bread and butter — Start here, not with low-level caching

Caching Hierarchy (prefer top over bottom):

HTTP Caching (stale?/fresh_when)     ← Fastest: never even hits your app
Fragment Caching (cache helper)       ← Fast: skips view rendering
Collection Caching (cached: true)     ← Fast: batch cache reads
Low-Level Caching (Rails.cache)       ← Flexible: cache any computation
SQL Caching (automatic per-request)   ← Free: Rails does this for you

When To Use This Skill

  • Adding caching to views, partials, or expensive computations
  • Setting up cache stores (Solid Cache, Redis, Memcached)
  • Implementing Russian doll caching with proper invalidation
  • Adding HTTP conditional GET support (ETags, Last-Modified)
  • Debugging stale cache issues or cache invalidation problems
  • Optimizing performance with caching strategies

Instructions

Step 1: Determine What to Cache

Before writing any cache code, identify the bottleneck:

# Check logs for slow queries or renders
grep "Completed" log/development.log | sort -t= -k2 -rn | head -20

# Use rack-mini-profiler or Rails server timing
# Look for: slow partials, N+1 queries, expensive computations

Good candidates for caching:

  • Partials rendered in loops (product cards, comment lists)
  • Expensive computations (reports, aggregations, API calls)
  • Rarely-changing content (navigation, footer, settings)
  • External API responses

Bad candidates for caching:

  • Content that changes every request (CSRF tokens, flash messages)
  • User-specific content without user-scoped keys
  • Content behind authentication without proper key scoping
  • Anything that writes to the database

Step 2: Choose the Right Cache Strategy

Strategy Use When Cache Key Based On
Fragment cache Caching a chunk of view HTML Record + template digest
Collection cache Rendering a collection of partials Each record individually
Russian doll Nested fragments with shared invalidation Record hierarchy with touch
Low-level cache Caching computation results, API calls Custom key you define
Conditional GET Entire response hasn't changed ETag or Last-Modified

Step 3: Implement Fragment Caching

Basic fragment cache:

<% cache product do %>
  <%= render product %>
<% end %>

Rails generates a key like: views/products/show:abc123/products/1-20260101120000000000

The key includes:

  • Template tree digest (changes when template changes)
  • Record's cachekeywith_version (changes when record updates)

Conditional caching:

<% cache_if !current_user&.admin?, product do %>
  <%= render product %>
<% end %>

Multi-part cache keys for user-specific or locale-specific content:

<% cache [current_user, I18n.locale, product] do %>
  <%= render product %>
<% end %>

Include the user in the cache key when content varies by user. Without it, you'll cache "Welcome, Ryan" and serve it to every visitor — this is the most common caching mistake.

Step 4: Use Collection Caching for Lists

Instead of caching each item in a loop, use collection caching:

<%# SLOW — one cache read per item %>
<% @products.each do |product| %>
  <% cache product do %>
    <%= render partial: "products/product", locals: { product: product } %>
  <% end %>
<% end %>

<%# FAST — batch cache read (multi-fetch) %>
<%= render partial: "products/product", collection: @products, cached: true %>

Collection caching reads all cache entries in one round trip. This is significantly faster for large collections.

Custom cache keys for collections:

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

Step 5: Implement Russian Doll Caching

Russian doll = nested cache fragments where inner cache busting propagates outward via touch.

The view:

<% cache @category do %>
  <h2><%= @category.name %></h2>
  <% @category.products.each do |product| %>
    <% cache product do %>
      <%= render product %>
    <% end %>
  <% end %>
<% end %>

The models — touch: true propagates cache invalidation up the chain:

class Category < ApplicationRecord
  has_many :products
end

class Product < ApplicationRecord
  belongs_to :category, touch: true
end

When a product updates → its updatedat changes → touch: true updates category's updatedat → both cache fragments expire.

Without touch: true, the outer cache serves stale data — updating a product won't change the category's updatedat, so the category fragment never expires. Every belongsto in a Russian doll chain needs touch: true.

Multi-level touch chains:

class Store < ApplicationRecord
  has_many :categories
end

class Category < ApplicationRecord
  belongs_to :store, touch: true
  has_many :products
end

class Product < ApplicationRecord
  belongs_to :category, touch: true
  # Updating a product touches category, which touches store
end

Step 6: Low-Level Caching with Rails.cache

Use fetch — it handles read + write in one call:

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

Key rules for low-level caching:

  1. Use cachekeywith_version for AR records — it auto-invalidates when the record updates
  2. Set expires_in — unbounded caches accumulate stale data that's hard to debug
  3. Cache IDs or primitives, not Active Record objects — cached AR objects become stale and break on code reloads
# Avoid — cached AR objects go stale and break on code reloads
Rails.cache.fetch("super_admins", expires_in: 12.hours) do
  User.super_admins.to_a
end

# Better — cache IDs, re-query fresh
ids = Rails.cache.fetch("super_admin_ids", expires_in: 12.hours) do
  User.super_admins.pluck(:id)
end
User.where(id: ids)

Other cache operations:

Rails.cache.read("key")                    # Returns nil on miss
Rails.cache.write("key", value, expires_in: 1.hour)
Rails.cache.delete("key")
Rails.cache.exist?("key")
Rails.cache.increment("counter")
Rails.cache.decrement("counter")

# Delete by pattern (not supported by all stores)
Rails.cache.delete_matched("products/*")

# fetch_multi for batch operations
Rails.cache.fetch_multi("key1", "key2", "key3", expires_in: 1.hour) do |key|
  expensive_computation(key)
end

Step 7: HTTP Conditional GET (stale? / fresh_when)

This is the most impactful cache — it prevents the response from being generated at all.

Use stale? when you have custom response logic:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    if stale?(@product)
      respond_to do |format|
        format.html
        format.json { render json: @product }
      end
    end
    # If fresh, Rails auto-sends 304 Not Modified
  end
end

Use fresh_when for simple cases (default template rendering):

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when @product
    # That's it — renders template if stale, 304 if fresh
  end

  def index
    @products = Product.all
    fresh_when @products
  end
end

With explicit options:

fresh_when last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version

For content that never changes:

def show
  http_cache_forever(public: true) do
    render
  end
end

When to use conditional GET:

  • Show pages for individual records
  • Index pages with predictable update patterns
  • API endpoints where clients cache responses
  • Static-ish pages (about, terms, etc.)

Step 8: Configure Cache Store

Solid Cache (Rails 8 Default — Use This)

Database-backed caching using SSDs. No extra infrastructure needed.

# config/environments/production.rb
config.cache_store = :solid_cache_store
# config/database.yml
production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate
# config/cache.yml
default: &default
  store_options:
    max_age: <%= 60.days.to_i %>
    max_size: <%= 256.megabytes %>
    namespace: <%= Rails.env %>

Why Solid Cache:

  • Zero infrastructure — it's just your database
  • Larger storage than RAM-based stores (SSDs are cheap)
  • FIFO eviction with configurable max_age
  • Supports encryption for sensitive cached data
  • Supports sharding for horizontal scaling

Other stores: Redis (:rediscachestore), Memory (:memorystore), Null (:nullstore). See references/cache-stores.md for Redis production configuration with error handling, and full store comparison.

Step 9: Enable Caching in Development

bin/rails dev:cache    # Toggle caching on/off

This creates/removes tmp/caching-dev.txt and toggles perform_caching.

To use Solid Cache in development:

# config/environments/development.rb
config.cache_store = :solid_cache_store

And add the cache database to config/database.yml under development.

Step 10: Test Cache Behavior

Test that caching works correctly, not that Rails caching works:

class ProductTest < ActiveSupport::TestCase
  test "competing_price is cached" do
    product = products(:widget)

    # First call hits the API
    assert_equal 29.99, product.competing_price

    # Verify cache was written
    assert Rails.cache.exist?("#{product.cache_key_with_version}/competing_price")
  end

  test "touch propagation invalidates parent cache" do
    category = categories(:electronics)
    product = products(:widget)

    old_updated_at = category.updated_at
    product.update!(name: "New Name")

    assert_operator category.reload.updated_at, :>, old_updated_at
  end
end

In request tests, verify conditional GET:

class ProductsControllerTest < ActionDispatch::IntegrationTest
  test "returns 304 when product unchanged" do
    product = products(:widget)

    get product_path(product)
    assert_response :success
    etag = response.headers["ETag"]

    get product_path(product), headers: { "HTTP_IF_NONE_MATCH" => etag }
    assert_response :not_modified
  end
end

Common Mistakes

1. Caching User-Specific Content Without User Key

<%# Wrong: all users see the same cached content %>
<% cache @dashboard do %>
  Welcome, <%= current_user.name %>!
  <%= render @dashboard.widgets %>
<% end %>

<%# Fixed: include user in cache key %>
<% cache [current_user, @dashboard] do %>
  Welcome, <%= current_user.name %>!
  <%= render @dashboard.widgets %>
<% end %>

2. Missing touch on Associations

# Problem: updating a comment doesn't expire the post cache
class Comment < ApplicationRecord
  belongs_to :post
end

# Fixed: touch propagates invalidation to parent
class Comment < ApplicationRecord
  belongs_to :post, touch: true
end

3. Caching Active Record Objects

# Problem: cached AR objects become stale and break on code reload
Rails.cache.fetch("featured") { Product.featured.to_a }

# Fixed: cache IDs, re-query fresh
ids = Rails.cache.fetch("featured_ids", expires_in: 1.hour) { Product.featured.pluck(:id) }
Product.where(id: ids)

4. No Expiration on Low-Level Cache

# Problem: cache lives forever, exchange rates go stale silently
Rails.cache.fetch("exchange_rates") { ExchangeRateAPI.current }

# Fixed: set expires_in so data refreshes
Rails.cache.fetch("exchange_rates", expires_in: 1.hour) { ExchangeRateAPI.current }

See references/fragment-caching.md for additional common mistakes including cache key dependency issues and helper digest gotchas.

Quick Reference: Cache Key Methods

Method Returns Use For
cache_key products/1 Stable key (no version)
cachekeywith_version products/1-20260101120000 Key + timestamp version
cache_version 20260101120000 Just the version stamp
to_param "1" Fallback for non-AR objects

Reference

For detailed patterns, edge cases, and advanced configuration, see the references/ directory:

  • references/fragment-caching.md — Cache keys, composite keys, collection caching, dependencies
  • references/russian-doll.md — Nested caching, touch propagation, counter caches
  • references/low-level-caching.md — Rails.cache.fetch, fetch_multi, atomic operations
  • references/conditional-get.md — stale?, fresh_when, ETags, HTTP caching
  • references/cache-stores.md — Store comparison, Solid Cache, Redis, Memcached config
  • references/performance.md — N+1 cache calls, warming, stampede prevention, debugging