Rails 8 introduced Solid Cache, a database-backed caching solution that eliminates the need for Redis or Memcached in many applications. This simplifies infrastructure, reduces costs, and leverages the durability of MySQL for cache storage.
Why Database-Backed Caching?
Traditional caching solutions like Redis add operational complexity: another service to monitor, scale, and maintain. Solid Cache stores cache entries directly in MySQL, which most Rails applications already use. Modern SSDs make database-backed caching surprisingly fast, and MySQL's reliability means cache data survives restarts.
Solid Cache works best for applications where cache hits don't require sub-millisecond latency. For most web applications—caching fragments, API responses, or computed values—MySQL performs exceptionally well.
Setting Up Solid Cache
Rails 8 applications created with the default configuration include Solid Cache automatically. For existing applications, add the gem and run the installer:
# Gemfile
gem "solid_cache"
# Terminal
$ bundle install
$ bin/rails solid_cache:install
The installer creates a migration for the cache table and updates the configuration. Run the migration to create the necessary table:
# db/migrate/20260106000000_create_solid_cache_entries.rb
class CreateSolidCacheEntries < ActiveRecord::Migration[8.0]
def change
create_table :solid_cache_entries do |t|
t.binary :key, null: false, limit: 1024
t.binary :value, null: false, limit: 536870912
t.datetime :created_at, null: false
t.index :key, unique: true
t.index :created_at
end
end
end
Configure the cache store in the environment file:
# config/environments/production.rb
Rails.application.configure do
config.cache_store = :solid_cache_store
end
Configuring Cache Behavior
Solid Cache offers several configuration options for controlling cache size and cleanup. Create a dedicated configuration file:
# config/solid_cache.yml
default: &default
database: cache
store_options:
max_age: <%= 1.week.to_i %>
max_size: <%= 256.megabytes %>
namespace: <%= Rails.env %>
development:
<<: *default
store_options:
max_size: <%= 64.megabytes %>
production:
<<: *default
store_options:
max_size: <%= 1.gigabyte %>
max_age: <%= 2.weeks.to_i %>
For larger applications, Solid Cache supports a dedicated database connection. This prevents cache operations from competing with primary database queries:
# config/database.yml
production:
primary:
adapter: mysql2
database: myapp_production
host: <%= ENV["DB_HOST"] %>
cache:
adapter: mysql2
database: myapp_cache_production
host: <%= ENV["CACHE_DB_HOST"] %>
migrations_paths: db/cache_migrate
Practical Caching Patterns
Fragment caching with Solid Cache works identically to other cache stores. The performance characteristics favor caching larger fragments rather than many small ones:
# app/views/dashboard/show.html.erb
<%% cache ["dashboard", current_user, current_user.projects.maximum(:updated_at)] do %>
Active Projects
<%%= current_user.projects.active.count %>
Pending Tasks
<%%= current_user.tasks.pending.count %>
<%% current_user.projects.active.each do |project| %>
<%%= render project %>
<%% end %>
<%% end %>
Low-level caching handles computed values and API responses effectively. Use fetch blocks to compute values only when the cache misses:
# app/models/report.rb
class Report < ApplicationRecord
belongs_to :organization
def summary_statistics
Rails.cache.fetch(cache_key_for_statistics, expires_in: 1.hour) do
{
total_revenue: calculate_revenue,
growth_rate: calculate_growth_rate,
top_products: fetch_top_products,
generated_at: Time.current
}
end
end
def invalidate_statistics_cache
Rails.cache.delete(cache_key_for_statistics)
end
private
def cache_key_for_statistics
["report_statistics", id, updated_at.to_i]
end
def calculate_revenue
line_items.sum(:amount)
end
def calculate_growth_rate
# Complex calculation
end
def fetch_top_products
line_items.group(:product_id)
.order("SUM(amount) DESC")
.limit(10)
.pluck(:product_id)
end
end
Cache Expiration Strategies
Solid Cache automatically evicts entries based on age and size limits. However, explicit invalidation ensures data freshness. Use callbacks to clear related caches:
# app/models/product.rb
class Product < ApplicationRecord
belongs_to :category
after_commit :invalidate_caches
private
def invalidate_caches
Rails.cache.delete(["product", id])
Rails.cache.delete(["category_products", category_id])
Rails.cache.delete_matched("products_list_*")
end
end
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Rails.cache.fetch(
["products_list", params[:page], params[:category]],
expires_in: 15.minutes
) do
Product.includes(:category)
.where(category_id: params[:category])
.page(params[:page])
.to_a
end
end
end
Monitoring and Maintenance
Monitor cache table size and hit rates to optimize configuration. Create a simple health check endpoint:
# app/controllers/health_controller.rb
class HealthController < ApplicationController
def cache_status
stats = {
entries_count: SolidCache::Entry.count,
oldest_entry: SolidCache::Entry.minimum(:created_at),
newest_entry: SolidCache::Entry.maximum(:created_at),
estimated_size: SolidCache::Entry.sum("LENGTH(value)")
}
render json: stats
end
end
For MySQL, ensure the cache table uses appropriate settings for write-heavy workloads:
# db/migrate/20260106000001_optimize_solid_cache_table.rb
class OptimizeSolidCacheTable < ActiveRecord::Migration[8.0]
def up
execute "ALTER TABLE solid_cache_entries ROW_FORMAT=COMPRESSED"
end
def down
execute "ALTER TABLE solid_cache_entries ROW_FORMAT=DEFAULT"
end
end
Common Mistakes to Avoid
Caching database queries directly returns stale Active Record objects. Always convert to plain data structures:
# Wrong: Caches Active Record objects
Rails.cache.fetch("users") { User.all }
# Correct: Caches serializable data
Rails.cache.fetch("users") { User.all.to_a }
# Better: Cache only what's needed
Rails.cache.fetch("user_names") { User.pluck(:id, :name).to_h }
Avoid cache keys that change too frequently. Include only the attributes that affect the cached content:
# Too volatile: updated_at changes on every save
cache_key = ["user", user.id, user.updated_at]
# More stable: version only relevant attributes
cache_key = ["user_profile", user.id, user.avatar_updated_at, user.bio_updated_at]
Summary
Solid Cache provides a production-ready caching solution without external dependencies. The setup process involves adding the gem, running migrations, and configuring the cache store. For most applications, the default settings work well, with optional tuning for cache size and expiration policies.
Consider a dedicated cache database for high-traffic applications, and monitor cache table growth to adjust size limits. Combined with proper invalidation strategies, Solid Cache delivers reliable caching backed by MySQL's durability.