Caching in Rails has traditionally meant choosing between Redis or Memcached. Both are fast but expensive when we need large caches. Memory costs add up quickly.
Rails 8 introduces Solid Cache as the default production cache store. It stores cache entries in the database, leveraging modern SSD performance to provide larger caches at significantly lower costs.
In this post, we will explore how Solid Cache works, how to configure it for production, and important tradeoffs to consider.
The Case for Database Backed Caching
Traditional memory based caches have a fundamental limitation: RAM is expensive. A 10GB Redis cache costs significantly more than 10GB of SSD storage.
Modern NVMe SSDs have changed the performance equation. Read latencies are now measured in microseconds, making disk based caching viable for most use cases.
Solid Cache takes advantage of this shift. We can now maintain caches measured in hundreds of gigabytes at a fraction of the cost of equivalent memory based solutions.
Installation
Solid Cache is preconfigured in new Rails 8 applications. For existing applications on Rails 7.1 or later:
bundle add solid_cache
bin/rails solid_cache:installThis creates:
config/cache.ymlfor cache configurationdb/cache_schema.rbfor the database schema
Database Configuration
Like Solid Queue, Solid Cache works best with a dedicated database. Here is a SQLite configuration:
# config/database.yml
production:
primary:
<<: *default
database: storage/production.sqlite3
cache:
<<: *default
database: storage/production_cache.sqlite3
migrations_paths: db/cache_migrateFor PostgreSQL:
# config/database.yml
production:
primary:
<<: *default
database: myapp_production
cache:
<<: *default
database: myapp_cache_production
migrations_paths: db/cache_migrateRun the setup:
bin/rails db:prepareConfiguration Options
Configure Solid Cache in config/cache.yml:
# config/cache.yml
production:
database: cache
store_options:
max_age: 1.week
max_size: 256.megabytes
namespace: myapp_prod
compress: true
compress_threshold: 1.kilobyteKey options explained:
max_age: Maximum time entries stay in cache before expirationmax_size: Maximum total cache size (triggers cleanup when exceeded)max_entries: Alternative to max_size, limits by entry countcompress: Enable compression for large valuescompress_threshold: Minimum size before compression kicks in
Real World Usage Examples
Fragment Caching
Solid Cache works seamlessly with Rails fragment caching:
<%# app/views/products/show.html.erb %>
<% cache @product do %>
<div class="product-details">
<h1><%= @product.name %></h1>
<p><%= @product.description %></p>
<%= render @product.reviews %>
</div>
<% end %>Low Level Caching
For complex data that is expensive to compute:
# app/models/dashboard.rb
class Dashboard
def self.metrics_for(user)
Rails.cache.fetch("dashboard/#{user.id}/metrics", expires_in: 15.minutes) do
{
total_orders: user.orders.count,
revenue: user.orders.sum(:total),
average_order: user.orders.average(:total),
recent_activity: user.activities.recent.to_a
}
end
end
endRussian Doll Caching
Solid Cache handles nested caching efficiently:
<%# app/views/categories/show.html.erb %>
<% cache @category do %>
<h1><%= @category.name %></h1>
<% @category.products.each do |product| %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<% end %>When a product updates, only its fragment expires. The category fragment rebuilds with the new product cache.
API Response Caching
Cache expensive API responses:
# app/controllers/api/reports_controller.rb
class Api::ReportsController < ApplicationController
def sales
data = Rails.cache.fetch("api/reports/sales/#{Date.current}", expires_in: 1.hour) do
SalesReport.generate(
start_date: 30.days.ago,
end_date: Date.current
)
end
render json: data
end
endCache Expiration Strategy
Solid Cache uses a FIFO (First In First Out) expiration strategy. This differs from LRU (Least Recently Used) caches like Redis.
The tradeoff is simpler implementation and better database performance. With large cache sizes enabled by Solid Cache, the difference is negligible for most applications.
Expiration happens automatically when:
- Entries exceed
max_age - Total size exceeds
max_size - Total entries exceed
max_entries
Configure the expiration batch size:
# config/cache.yml
production:
store_options:
expiry_batch_size: 100
expiry_method: :jobSetting expiry_method: :job runs cleanup in a background job
instead of inline during writes.
This improves write performance for high throughput applications.
Sharding for Scale
For very large caches, Solid Cache supports sharding across multiple databases:
# config/database.yml
production:
cache_shard_1:
<<: *default
database: myapp_cache_1
cache_shard_2:
<<: *default
database: myapp_cache_2# config/environments/production.rb
config.solid_cache.connects_to = {
shards: {
shard_1: { writing: :cache_shard_1 },
shard_2: { writing: :cache_shard_2 }
}
}# config/cache.yml
production:
store_options:
cluster:
- shards: [shard_1, shard_2]Solid Cache uses consistent hashing to distribute keys across shards.
Encryption
For sensitive cached data, enable encryption:
# config/cache.yml
production:
store_options:
encrypt: trueThis requires Active Record Encryption to be configured in the application.
Caveats and Considerations
Latency Compared to Redis
Solid Cache is slower than Redis for individual operations. Typical latencies:
- Redis: 0.1 to 0.5 milliseconds
- Solid Cache: 1 to 5 milliseconds
For most web applications, this difference is imperceptible. However, if the application makes hundreds of cache calls per request, the latency adds up.
Profile the application before switching.
Database Connection Pool
Solid Cache uses database connections from the pool. Ensure adequate connections:
# config/database.yml
production:
cache:
<<: *default
database: myapp_cache
pool: <%= ENV.fetch("CACHE_DB_POOL", 10) %>Write Amplification
Every cache write is a database write. High write volumes can impact database performance and increase storage I/O.
Monitor the database metrics after enabling Solid Cache.
FIFO vs LRU
The FIFO expiration strategy means frequently accessed items can be evicted before rarely accessed ones. This is usually fine with large caches but can cause unexpected cache misses if the cache size is constrained.
No Pub/Sub
Unlike Redis, Solid Cache does not support pub/sub patterns. If we use Redis for both caching and real time features, we may still need Redis for the latter.
Backup Considerations
The cache is now part of the database. Consider whether we need to include it in backups. For most applications, cache data is regenerable and can be excluded from backups to save space.
Cold Start Performance
After a deployment or restart, the cache is empty. This can cause a thundering herd problem where many requests simultaneously try to populate the cache.
Implement cache warming for critical data:
# lib/tasks/cache.rake
namespace :cache do
desc "Warm critical caches"
task warm: :environment do
Product.featured.find_each do |product|
Rails.cache.fetch("products/#{product.id}") { product.to_cache_hash }
end
Category.active.find_each do |category|
Rails.cache.fetch("categories/#{category.id}") { category.to_cache_hash }
end
end
endMigration from Redis
Migrating from Redis cache is straightforward:
- Install Solid Cache
- Configure the cache database
- Update environment configuration
- Deploy and monitor
# config/environments/production.rb
# Before
config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
# After
config.cache_store = :solid_cache_storeThe cache will be empty after migration. Plan for cache warming or accept temporary performance degradation.
When to Use Solid Cache
Solid Cache is ideal when:
- We want to reduce infrastructure complexity
- We need large caches (tens or hundreds of gigabytes)
- Cost is a concern
- Individual cache latency is not critical
Consider alternatives when:
- We need sub millisecond cache latency
- The application makes many cache calls per request
- We already have Redis for other features
- We need pub/sub or other Redis specific features
Performance Tuning
Compression
Enable compression for large values:
# config/cache.yml
production:
store_options:
compress: true
compress_threshold: 1.kilobyteThis reduces storage and I/O at the cost of CPU.
Connection Pooling
Use a connection pool sized for the workload:
# config/database.yml
production:
cache:
pool: <%= ENV.fetch("CACHE_DB_POOL", 20) %>Index Optimization
Solid Cache creates appropriate indexes automatically. For very large caches, monitor query performance and consider database specific optimizations.
Conclusion
Solid Cache brings database backed caching to Rails 8, enabling larger caches at lower costs. It integrates seamlessly with existing Rails caching patterns and requires minimal configuration changes.
The tradeoffs around latency and write amplification are acceptable for most applications. Profile the specific workload to ensure Solid Cache meets the needs.
Check the official Solid Cache repository for the latest documentation and configuration options.
