Caching in Rails: Boost Performance with Simple Techniques
📚 Table of Contents
- 🧠 What is Caching?
- 🚀 Rails Caching Basics
- 🧩 Fragment Caching (Most Common)
- 🔧 Low-Level Caching
- 🗝️ Cache Keys and Versioning
- 🎪 Russian Doll Caching
- ⚙️ Cache Store Configuration
- 📝 View Caching (Template Digest-based)
- 🌐 HTTP Caching (Conditional GET)
- ⏰ Cache Expiration Strategies
- 📊 Performance Monitoring
- 🔒 Cache Security & Best Practices
- 🚀 Production Deployment
- 🧪 Testing Caching
- 🎯 Interview Preparation
- 🧪 Alternatives
🧠 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
🚀 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
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
becomesusers/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
- 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
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 %>
- 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
andcache_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
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
"user_#{user.id}_recent_orders"
instead of "data"
"v2_user_#{user.id}_profile"
["products", "featured", "limit_10"]
"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
delete_matched
for related caches, set up model callbacks- 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 ofcache "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
- 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
Best Practices
- 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 Type | Speed | Persistence | Memory | Use Case |
---|---|---|---|---|
Memory | Fastest | No | RAM | Development |
File | Slow | Yes | Disk | Small apps |
Redis | Very Fast | Yes | RAM | Production |
Memcached | Fast | No | RAM | Legacy 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
- 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
- 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
andcache_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
- 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
- ✅ 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
- ✅ 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
- ✅ 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
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
Start sharing, start earning—become our affiliate today! https://shorturl.fm/Yesy3
Start earning instantly—become our affiliate and earn on every sale! https://shorturl.fm/wyh4E
Join our affiliate community and maximize your profits! https://shorturl.fm/MjNVU
Partner with us and enjoy recurring commission payouts! https://shorturl.fm/z7JVD
Monetize your traffic with our affiliate program—sign up now! https://shorturl.fm/XNTFA
Apply now and receive dedicated support for affiliates! https://shorturl.fm/YsAP8
Become our partner and turn referrals into revenue—join now! https://shorturl.fm/eUfak
Join our affiliate community and maximize your profits—sign up now! https://shorturl.fm/xhVMs
Unlock top-tier commissions—become our affiliate partner now! https://shorturl.fm/GTbuh
Tap into a new revenue stream—become an affiliate partner! https://shorturl.fm/OksvS
Become our partner and turn clicks into cash—join the affiliate program today! https://shorturl.fm/JWkCI
Invite your network, boost your income—sign up for our affiliate program now! https://shorturl.fm/RgkYd
Share our products, reap the rewards—apply to our affiliate program! https://shorturl.fm/zE1YK
Turn your traffic into cash—join our affiliate program! https://shorturl.fm/Tg6mv
Turn your network into income—apply to our affiliate program! https://shorturl.fm/US5Td
Earn passive income with every click—sign up today! https://shorturl.fm/Huhcs
Join our affiliate community and maximize your profits! https://shorturl.fm/DHBlg
Your audience, your profits—become an affiliate today! https://shorturl.fm/1psgs
Partner with us and earn recurring commissions—join the affiliate program! https://shorturl.fm/bscvc
Start sharing our link and start earning today! https://shorturl.fm/53AER
Drive sales and watch your affiliate earnings soar! https://shorturl.fm/i0Aer
Become our partner and turn referrals into revenue—join now! https://shorturl.fm/ryv45
Refer and earn up to 50% commission—join now! https://shorturl.fm/v22bE
Join our affiliate family and watch your profits soar—sign up today! https://shorturl.fm/kIsmH
Start profiting from your network—sign up today! https://shorturl.fm/lvamt
Turn your audience into earnings—become an affiliate partner today! https://shorturl.fm/CDTco
Share our products and watch your earnings grow—join our affiliate program! https://shorturl.fm/J19tk
Start profiting from your network—sign up today! https://shorturl.fm/ZsNOx
Share our products and watch your earnings grow—join our affiliate program! https://shorturl.fm/DLbrJ
Refer friends and colleagues—get paid for every signup! https://shorturl.fm/qwPoG
https://shorturl.fm/hYmTk
https://shorturl.fm/L9dG7
https://shorturl.fm/lH9Fv
https://shorturl.fm/NtEkm
https://shorturl.fm/ycMqz
https://shorturl.fm/xzWMG
https://shorturl.fm/pe26J
https://shorturl.fm/HX2ZD
https://shorturl.fm/AyzXs
https://shorturl.fm/SXr6N
https://shorturl.fm/AlCmW
https://shorturl.fm/7NlED
https://shorturl.fm/pgYmA
https://shorturl.fm/DQa8b
https://shorturl.fm/ajcMh
https://shorturl.fm/Zf69I
https://shorturl.fm/rCitB
https://shorturl.fm/7FKN0
https://shorturl.fm/oFs3Q
https://shorturl.fm/NUggB
https://shorturl.fm/yAaQ4
https://shorturl.fm/KfUnS
https://shorturl.fm/lGQSe
https://shorturl.fm/iVFWu
https://shorturl.fm/o9B7e
https://shorturl.fm/UYGJZ
https://shorturl.fm/cCPfG