Rails 8 Solid Cache Performance: Complete Migration from Redis
Rails 8 introduces Solid Cache as the default caching backend, marking a significant shift from Redis-based caching to database-backed storage. This comprehensive guide explores Solid Cache performance characteristics, migration strategies from Redis/Memcached, and optimization techniques for production deployments.
Executive Summary #
Solid Cache leverages your existing database for caching, eliminating external dependencies while providing reliable, cost-effective performance. Redis offers superior speed for cache-intensive applications but requires dedicated infrastructure.
Quick Decision Framework: #
- Choose Solid Cache for: Simplified operations, cost reduction, moderate cache hit rates (<10,000 reads/sec)
- Choose Redis for: High-frequency caching (>10,000 reads/sec), sub-millisecond latency requirements, established Redis infrastructure
Why Database-Backed Caching Matters #
The Infrastructure Simplification Story #
Traditional Rails caching requires Redis or Memcached infrastructure, adding operational complexity:
Traditional Caching Architecture: #
Infrastructure Requirements:
- Rails application servers
- PostgreSQL database
- Redis cache cluster
- Redis monitoring and backup
- Network configuration between services
- Additional security considerations
Monthly Costs (typical mid-size app):
- Redis hosting: $200-500/month
- Redis monitoring: $50-100/month
- DevOps overhead: 5-10 hours/month
Solid Cache Architecture: #
Simplified Infrastructure:
- Rails application servers
- PostgreSQL database (with cache tables)
Monthly Costs:
- Additional database storage: $10-30/month
- DevOps overhead: <1 hour/month
Real-World Impact: Cost Savings Analysis #
Case Study: E-commerce Platform Migration #
Before (Redis):
- Redis hosting: $350/month
- Redis backups: $75/month
- Monitoring tools: $50/month
- DevOps time: 8 hours/month ($400)
- Total: $875/month
After (Solid Cache):
- Additional database storage: $20/month
- DevOps time: 0.5 hours/month ($25)
- Total: $45/month
Annual savings: $9,960 with negligible performance impact for moderate cache hit rates.
Note: These cost estimates are for the specified e-commerce platform scenario. Actual costs depend on hosting provider, data transfer, storage rates, and labor rates in your region. Benchmark with your specific infrastructure and regional pricing.
Solid Cache Architecture Deep Dive #
Database-Backed Caching Fundamentals #
Solid Cache uses advanced database features to provide efficient caching:
# Core Solid Cache implementation
module SolidCache
class Entry < ActiveRecord::Base
# Efficient key-value storage with expiration
# Uses database indexes for fast lookups
# Leverages database transactions for consistency
end
end
# Automatic configuration in Rails 8
# config/environments/production.rb
config.cache_store = :solid_cache_store
# Advanced configuration options
config.cache_store = :solid_cache_store, {
database: :cache, # Use separate cache database
expires_in: 2.weeks, # Default expiration
size_estimate: 100.megabytes, # Size hint for optimization
cleanup_interval: 1.day # Automatic cleanup frequency
}
Key Architecture Benefits: #
- Transactional Consistency
# Cache updates are transactional with database changes
ActiveRecord::Base.transaction do
user.update!(premium: true)
# Cache invalidation happens in same transaction
Rails.cache.delete("user:#{user.id}:status")
# No risk of stale cache if transaction rolls back
end
- Automatic Cleanup and Eviction
# Solid Cache handles expiration automatically
# No manual eviction policies needed like Redis
class CacheManager
def store_with_expiration(key, value, ttl)
Rails.cache.write(
key,
value,
expires_in: ttl,
race_condition_ttl: 10.seconds
)
# Database automatically removes expired entries
end
end
- No Memory Pressure
# Cache stored in database, not memory
# No need to monitor memory usage
# No risk of cache eviction under memory pressure
class LargeCacheHandler
def cache_bulk_data(dataset)
# Can cache large datasets without memory concerns
dataset.each_slice(1000) do |slice|
Rails.cache.write(
"dataset:#{slice.first.id}",
slice.to_json,
expires_in: 1.hour
)
end
end
end
Performance Characteristics #
Solid Cache Performance Profile: #
| Operation | Solid Cache (PostgreSQL) | Redis | Difference |
|---|---|---|---|
| Read (cached) | 3-8ms | 0.5-2ms | 4-6x slower |
| Write | 5-12ms | 1-3ms | 3-4x slower |
| Delete | 4-10ms | 0.5-2ms | 5-8x slower |
| Bulk read (10 keys) | 15-30ms | 5-10ms | 2-3x slower |
| Cache hit rate | Same | Same | Equal |
| Storage capacity | Unlimited (disk) | Limited (memory) | Advantage Solid Cache |
Performance Trade-offs: #
# Scenarios where Solid Cache performs well
class SolidCacheOptimalScenarios
# 1. Moderate cache hit frequency (<1000 reads/sec)
def moderate_frequency_caching
# Perfect for page caching, fragment caching
Rails.cache.fetch("homepage:#{locale}", expires_in: 1.hour) do
render_homepage_expensive_operation
end
end
# 2. Large cached data
def large_data_caching
# Can cache large datasets without memory concerns
Rails.cache.fetch("product_catalog:full", expires_in: 6.hours) do
Product.includes(:images, :variants).to_json
end
end
# 3. Infrequent cache invalidation
def stable_cache_patterns
# Excellent for data that changes infrequently
Rails.cache.fetch("configuration:global", expires_in: 24.hours) do
Configuration.global_settings.to_h
end
end
end
# Scenarios where Redis outperforms
class RedisOptimalScenarios
# 1. High-frequency caching (>10,000 reads/sec)
def high_frequency_caching
# API rate limiting, session storage
redis.get("rate_limit:user:#{user_id}:#{endpoint}")
end
# 2. Real-time features
def realtime_caching
# Live notifications, presence tracking
redis.smembers("online_users")
end
# 3. Complex data structures
def advanced_data_structures
# Sorted sets, pub/sub, hyperloglog
redis.zadd("leaderboard", score, user_id)
end
end
Migration Guide: Redis to Solid Cache #
Pre-Migration Assessment #
Evaluate your current Redis usage before migrating:
# Redis usage audit script
class RedisCacheAudit
def self.comprehensive_analysis
{
cache_hit_rate: measure_hit_rate,
cache_size: measure_cache_size,
access_patterns: analyze_access_patterns,
key_expiration: analyze_ttl_patterns,
read_write_ratio: measure_operations,
memory_usage: redis_memory_stats,
migration_readiness: assess_migration_complexity
}
end
private
def self.measure_hit_rate
info = Redis.current.info('stats')
hits = info['keyspace_hits'].to_f
misses = info['keyspace_misses'].to_f
(hits / (hits + misses) * 100).round(2)
end
def self.measure_cache_size
Redis.current.dbsize
end
def self.analyze_access_patterns
# Sample cache keys to understand patterns using SCAN (non-blocking)
sample_keys = []
cursor = "0"
loop do
cursor, batch = Redis.current.scan(cursor, match: "*", count: 100)
sample_keys.concat(batch)
break if cursor == "0" || sample_keys.size >= 100
end
{
page_caching: sample_keys.count { |k| k.start_with?('views/') },
fragment_caching: sample_keys.count { |k| k.start_with?('fragments/') },
query_caching: sample_keys.count { |k| k.include?('query') },
custom_caching: sample_keys.count { |k| !k.match(/views|fragments|query/) }
}
end
def self.analyze_ttl_patterns
# Use SCAN instead of KEYS to avoid blocking production Redis
keys = []
cursor = "0"
loop do
cursor, batch = Redis.current.scan(cursor, match: "*", count: 1000)
keys.concat(batch)
break if cursor == "0" || keys.size >= 1000
end
ttls = keys.map { |k| Redis.current.ttl(k) }
{
average_ttl: ttls.sum / ttls.size,
max_ttl: ttls.max,
no_expiry: ttls.count(-1)
}
end
def self.measure_operations
info = Redis.current.info('stats')
{
total_commands: info['total_commands_processed'],
reads: info['keyspace_hits'] + info['keyspace_misses'],
writes: estimate_writes(info),
ratio: calculate_ratio(info)
}
end
def self.redis_memory_stats
info = Redis.current.info('memory')
{
used_memory_human: info['used_memory_human'],
used_memory_peak_human: info['used_memory_peak_human'],
fragmentation_ratio: info['mem_fragmentation_ratio']
}
end
def self.assess_migration_complexity
# Determine migration difficulty
complexity_factors = {
redis_specific_features: uses_redis_specific_features?,
high_frequency_access: cache_hit_rate > 80,
large_cache_size: measure_cache_size > 100_000,
complex_ttl_patterns: complex_expiration_logic?
}
complexity_score = complexity_factors.values.count(true)
case complexity_score
when 0..1 then :easy_migration
when 2 then :moderate_migration
else :complex_migration_consider_hybrid
end
end
def self.uses_redis_specific_features?
# Check for sorted sets, pub/sub, etc. using SCAN (non-blocking)
redis = Redis.current
cursor = "0"
loop do
cursor, keys = redis.scan(cursor, count: 100)
keys.each do |key|
return true if redis.type(key) != 'string'
end
break if cursor == "0"
end
false
end
end
Step-by-Step Migration Process #
Phase 1: Setup Solid Cache Infrastructure #
# 1. Add solid_cache to Gemfile
# Gemfile
gem 'solid_cache'
# 2. Install and configure
bundle install
rails solid_cache:install:migrations
rails db:migrate
# 3. Configure cache store
# config/environments/production.rb
Rails.application.configure do
# Basic configuration
config.cache_store = :solid_cache_store
# Advanced configuration with separate database
config.cache_store = :solid_cache_store, {
database: :cache, # Use separate cache database
connects_to: { writing: :cache }, # Database connection
expires_in: 2.weeks, # Default TTL
size_estimate: 500.megabytes, # Size hint for optimization
namespace: "myapp_cache" # Namespace for multi-tenancy
}
end
Phase 2: Database Optimization for Caching #
# Create optimized indexes for cache performance
class OptimizeSolidCachePerformance < ActiveRecord::Migration[7.1]
def change
# 1. Composite index for key lookups with expiration
add_index :solid_cache_entries,
[:key, :expires_at],
name: 'index_solid_cache_on_key_expires',
where: 'expires_at IS NULL OR expires_at > NOW()'
# 2. Index for cleanup queries
add_index :solid_cache_entries,
:expires_at,
where: 'expires_at IS NOT NULL',
name: 'index_solid_cache_cleanup'
# 3. Partial index for active entries
add_index :solid_cache_entries,
[:key_hash, :byte_size],
where: 'expires_at IS NULL OR expires_at > NOW()',
name: 'index_solid_cache_active_entries'
end
end
# Configure separate cache database (optional but recommended)
# config/database.yml
production:
primary:
database: myapp_production
# ... primary database config
cache:
database: myapp_cache_production
migrations_paths: db/cache_migrate
# Use faster disk for cache database
# Consider using SSD or NVMe storage
Phase 3: Parallel Operation (Blue-Green Migration) #
# Run both caches simultaneously to validate
class DualCacheStrategy
def initialize
@solid_cache = ActiveSupport::Cache::SolidCacheStore.new
@redis_cache = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
end
def read(key, options = {})
# Read from both, compare results
solid_result = @solid_cache.read(key, options)
redis_result = @redis_cache.read(key, options)
# Log discrepancies for investigation
if solid_result != redis_result
Rails.logger.warn(
"Cache mismatch for key #{key}: " \
"Solid=#{solid_result.inspect}, Redis=#{redis_result.inspect}"
)
end
# Return Solid Cache result (new primary)
solid_result
end
def write(key, value, options = {})
# Write to both caches during migration
@solid_cache.write(key, value, options)
@redis_cache.write(key, value, options)
end
def delete(key, options = {})
@solid_cache.delete(key, options)
@redis_cache.delete(key, options)
end
def fetch(key, options = {}, &block)
# Fetch from Solid Cache, populate both
@solid_cache.fetch(key, options) do
value = block.call
@redis_cache.write(key, value, options)
value
end
end
end
# Use dual cache strategy
# config/environments/production.rb
Rails.application.configure do
config.cache_store = :memory_store, {
# Temporarily use custom dual-cache strategy
# Remove after validation period
}
end
Phase 4: Cache Warming Strategy #
# Warm up Solid Cache from Redis before cutover
class CacheWarmer
def self.warm_from_redis
redis = Redis.new(url: ENV['REDIS_URL'])
solid_cache = Rails.cache
# Use SCAN instead of KEYS to avoid blocking Redis
cursor = "0"
total_keys = 0
batch_count = 0
puts "Starting cache warming with SCAN batching..."
loop do
cursor, keys = redis.scan(cursor, count: 1000)
total_keys += keys.size
unless keys.empty?
ActiveRecord::Base.transaction do
keys.each do |key|
# Read from Redis
value = redis.get(key)
ttl = redis.ttl(key)
next unless value
# Write to Solid Cache with same TTL
solid_cache.write(
key,
value,
expires_in: ttl > 0 ? ttl.seconds : nil
)
end
end
batch_count += 1
puts "Processed batch #{batch_count} (#{total_keys} keys total)"
end
break if cursor == "0"
end
puts "Cache warming complete! Warmed #{total_keys} entries."
end
def self.verify_warmup
# Verify cache consistency using SCAN (non-blocking)
redis = Redis.new(url: ENV['REDIS_URL'])
solid_cache = Rails.cache
# Use SCAN instead of KEYS to avoid blocking production Redis
sample_keys = []
cursor = "0"
loop do
cursor, batch = redis.scan(cursor, match: "*", count: 100)
sample_keys.concat(batch)
break if cursor == "0" || sample_keys.size >= 100
end
mismatches = 0
sample_keys.each do |key|
redis_value = redis.get(key)
solid_value = solid_cache.read(key)
if redis_value != solid_value
mismatches += 1
puts "Mismatch for key #{key}"
end
end
puts "Verification complete: #{mismatches} mismatches out of #{sample_keys.size}"
end
end
# Run cache warming
rails runner "CacheWarmer.warm_from_redis"
rails runner "CacheWarmer.verify_warmup"
Phase 5: Cutover and Redis Decommission #
# 1. Final cache sync
rails runner "CacheWarmer.warm_from_redis"
# 2. Switch to Solid Cache in production
# config/environments/production.rb
config.cache_store = :solid_cache_store
# 3. Deploy application
bundle exec kamal deploy
# 4. Monitor cache performance
rails runner "CachePerformanceMonitor.start_monitoring"
# 5. After successful cutover (1-2 weeks), decommission Redis
# Remove Redis configuration
# Cancel Redis hosting
# Update Procfile to remove Redis dependencies
Migration Gotchas and Solutions #
Common Issues and Resolutions: #
- Database Connection Pool Exhaustion
# Problem: Cache reads consume database connections
# Solution: Increase connection pool size
# config/database.yml
production:
primary:
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + 10 %>
cache:
# Dedicated pool for cache operations
pool: <%= ENV.fetch("CACHE_POOL_SIZE", 20).to_i %>
- Cache Key Compatibility
# Problem: Redis key formats may differ from Solid Cache
# Solution: Normalize cache keys
class CacheKeyNormalizer
def self.normalize(key)
# Ensure consistent key format
key.to_s.gsub(/[^a-zA-Z0-9_\-:]/, '_')
end
end
# Wrapper around Rails.cache
module CacheHelper
def cache_write(key, value, options = {})
Rails.cache.write(
CacheKeyNormalizer.normalize(key),
value,
options
)
end
def cache_read(key, options = {})
Rails.cache.read(
CacheKeyNormalizer.normalize(key),
options
)
end
end
- Performance Regression Detection
# Implement comprehensive monitoring
class CachePerformanceMonitor
def self.track_operation(operation, key)
start_time = Time.current
result = yield
duration = (Time.current - start_time) * 1000 # ms
# Log slow cache operations
if duration > 50 # ms threshold
Rails.logger.warn(
"Slow cache #{operation} for key #{key}: #{duration.round(2)}ms"
)
end
# Send metrics to monitoring system
StatsD.increment("cache.#{operation}")
StatsD.timing("cache.#{operation}.duration", duration)
result
end
def self.start_monitoring
# Override Rails.cache methods to track performance
Rails.cache.singleton_class.prepend(CacheInstrumentation)
end
end
module CacheInstrumentation
def read(key, options = {})
CachePerformanceMonitor.track_operation(:read, key) do
super
end
end
def write(key, value, options = {})
CachePerformanceMonitor.track_operation(:write, key) do
super
end
end
end
Performance Optimization Strategies #
Database-Level Optimizations #
# 1. Table Partitioning for Large Caches
class PartitionSolidCacheTable < ActiveRecord::Migration[7.1]
def up
# Partition by month for automatic cleanup
execute <<-SQL
CREATE TABLE solid_cache_entries_partitioned (
LIKE solid_cache_entries INCLUDING ALL
) PARTITION BY RANGE (created_at);
-- Create monthly partitions
CREATE TABLE solid_cache_entries_2025_01
PARTITION OF solid_cache_entries_partitioned
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE solid_cache_entries_2025_02
PARTITION OF solid_cache_entries_partitioned
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- ... additional partitions
SQL
end
end
# 2. Vacuum and Analyze Scheduling
# config/initializers/cache_maintenance.rb
if Rails.env.production?
# Schedule regular maintenance
Rails.application.config.after_initialize do
Thread.new do
loop do
sleep 6.hours
# Vacuum cache tables to reclaim space
ActiveRecord::Base.connection.execute(
'VACUUM ANALYZE solid_cache_entries'
)
Rails.logger.info 'Solid Cache maintenance completed'
end
end
end
end
# 3. Read Replicas for Cache Reads
class ApplicationRecord < ActiveRecord::Base
# Use read replicas for cache reads
connects_to shards: {
default: { writing: :primary, reading: :primary_replica },
cache: { writing: :cache, reading: :cache_replica }
}
end
# Configure cache to use read replica
Rails.application.configure do
config.cache_store = :solid_cache_store, {
database: :cache,
connects_to: {
writing: :cache,
reading: :cache_replica # Read from replica for scalability
}
}
end
Application-Level Optimizations #
# 1. Multi-Read Optimization
class OptimizedCaching
# Batch cache reads to reduce database queries
def fetch_multiple(keys)
Rails.cache.read_multi(*keys) do |key|
# Only expensive operations for missing keys
yield key
end
end
# Example usage
def load_user_data(user_ids)
cache_keys = user_ids.map { |id| "user:#{id}:profile" }
fetch_multiple(cache_keys) do |key|
user_id = key.split(':')[1]
User.find(user_id).profile_data
end
end
end
# 2. Cache Layering (Hybrid Approach)
class LayeredCache
def initialize
@memory_cache = ActiveSupport::Cache::MemoryStore.new(size: 64.megabytes)
@solid_cache = Rails.cache
end
def fetch(key, options = {})
# Check memory cache first (fastest)
@memory_cache.fetch(key, expires_in: 5.minutes) do
# Fall back to database cache (slower but persistent)
@solid_cache.fetch(key, options) do
yield
end
end
end
end
# Use for high-frequency reads
class ProductCatalog
def self.cached_products
layered_cache = LayeredCache.new
layered_cache.fetch('products:catalog', expires_in: 1.hour) do
Product.active.includes(:images).to_a
end
end
end
# 3. Intelligent Cache Warming
class CacheWarmer
# Warm cache during off-peak hours
def self.warm_critical_paths
# Identify most-accessed cache keys
critical_keys = [
'homepage:en',
'products:featured',
'navigation:menu'
]
critical_keys.each do |key|
Rails.cache.fetch(key, force: true) do
# Re-generate cached content
send("generate_#{key.split(':').first}")
end
end
end
def self.schedule_warming
# Run during low-traffic periods
Whenever.set_cron_task('0 3 * * *', 'CacheWarmer.warm_critical_paths')
end
end
Cost Analysis: Solid Cache vs Redis #
Total Cost of Ownership Comparison #
Scenario: Mid-Size SaaS Application #
- 50,000 active users
- 1M cache reads/day
- 100K cache writes/day
- Cache size: 2GB average
Redis Total Costs (Annual): #
Infrastructure:
Redis Hosting (AWS ElastiCache): $350/month × 12 = $4,200
Redis Backups (RDB + AOF): $75/month × 12 = $900
Monitoring (DataDog/New Relic): $50/month × 12 = $600
Operational:
DevOps maintenance: 8 hours/month × $100/hour × 12 = $9,600
Incident response: 4 hours/quarter × $150/hour × 4 = $2,400
Capacity planning: 2 hours/quarter × $100/hour × 4 = $800
Total Annual Cost: $18,500
Solid Cache Total Costs (Annual): #
Infrastructure:
Additional DB storage (2GB): $10/month × 12 = $120
Database backup (incremental): $5/month × 12 = $60
Operational:
DevOps maintenance: 0.5 hours/month × $100/hour × 12 = $600
Incident response: Minimal (included in DB management)
Capacity planning: Minimal (scales with database)
Total Annual Cost: $780
Annual Savings: $17,720 (95.8% reduction)
Note: These cost estimates are for the specified mid-size SaaS scenario (50K users, 1M cache reads/day). Actual costs depend on hosting provider, data transfer, storage rates, and labor rates in your region. Benchmark with your specific infrastructure and regional pricing.
ROI Calculation for Migration #
# Migration cost calculator
class MigrationROI
def self.calculate(app_profile)
{
migration_costs: estimate_migration_costs(app_profile),
annual_savings: calculate_annual_savings(app_profile),
payback_period: calculate_payback_period(app_profile),
five_year_roi: calculate_five_year_roi(app_profile)
}
end
private
def self.estimate_migration_costs(profile)
{
development_time: profile[:complexity] * 40, # hours
testing_time: 20, # hours
deployment_time: 8, # hours
total_cost: (profile[:complexity] * 40 + 28) * profile[:hourly_rate]
}
end
def self.calculate_annual_savings(profile)
redis_annual = profile[:redis_monthly_cost] * 12
solid_cache_annual = profile[:db_additional_cost] * 12
redis_annual - solid_cache_annual
end
def self.calculate_payback_period(profile)
migration_cost = estimate_migration_costs(profile)[:total_cost]
monthly_savings = calculate_annual_savings(profile) / 12.0
(migration_cost / monthly_savings).ceil
end
def self.calculate_five_year_roi(profile)
migration_cost = estimate_migration_costs(profile)[:total_cost]
total_savings = calculate_annual_savings(profile) * 5
((total_savings - migration_cost) / migration_cost * 100).round(2)
end
end
# Example calculation
app_profile = {
complexity: 3, # 1=simple, 5=complex
hourly_rate: 100,
redis_monthly_cost: 350,
db_additional_cost: 20
}
roi = MigrationROI.calculate(app_profile)
# => {
# migration_costs: { total_cost: 14800 },
# annual_savings: 3960,
# payback_period: 4, # months
# five_year_roi: 33.78 # percent
# }
Monitoring and Performance Tracking #
Comprehensive Solid Cache Monitoring #
# Custom monitoring dashboard
class SolidCacheMetrics
def self.collect_metrics
{
cache_stats: cache_statistics,
performance_metrics: performance_analysis,
database_impact: database_load_analysis,
capacity_metrics: capacity_planning_data
}
end
private
def self.cache_statistics
total_entries = SolidCache::Entry.count
active_entries = SolidCache::Entry.where('expires_at IS NULL OR expires_at > NOW()').count
expired_entries = total_entries - active_entries
{
total_entries: total_entries,
active_entries: active_entries,
expired_entries: expired_entries,
hit_rate: calculate_hit_rate,
average_entry_size: calculate_avg_size
}
end
def self.performance_analysis
# Track cache operation latencies
operations = [:read, :write, :delete]
operations.each_with_object({}) do |op, metrics|
metrics[op] = {
p50: fetch_percentile(op, 50),
p95: fetch_percentile(op, 95),
p99: fetch_percentile(op, 99),
average: fetch_average(op)
}
end
end
def self.database_load_analysis
# Measure impact on database performance
cache_queries = ActiveRecord::QueryRecorder.new do
10.times { Rails.cache.read('sample_key') }
end
{
queries_per_read: cache_queries.count / 10.0,
avg_query_time: cache_queries.log.sum(&:duration) / cache_queries.count,
connection_pool_usage: ActiveRecord::Base.connection_pool.stat
}
end
def self.capacity_planning_data
{
current_size: calculate_total_size,
growth_rate: calculate_growth_rate,
projected_size_30d: project_size(30.days),
estimated_cost: estimate_storage_cost
}
end
def self.calculate_hit_rate
# Implement hit rate tracking with custom instrumentation
# Note: Rails.cache.stats is not universally supported (including Solid Cache)
# Use ActiveSupport::Notifications for portable hit rate tracking
cache_hits = @cache_hits_counter ||= 0
cache_misses = @cache_misses_counter ||= 0
total = cache_hits + cache_misses
total > 0 ? (cache_hits.to_f / total * 100).round(2) : 0
end
# Track cache hits/misses with ActiveSupport::Notifications
ActiveSupport::Notifications.subscribe('cache_read.active_support') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
if event.payload[:hit]
@cache_hits_counter ||= 0
@cache_hits_counter += 1
else
@cache_misses_counter ||= 0
@cache_misses_counter += 1
end
end
end
# Expose metrics endpoint
class MetricsController < ApplicationController
def cache_metrics
render json: SolidCacheMetrics.collect_metrics
end
end
When to Keep Redis (Hybrid Approach) #
Strategic Hybrid Architecture #
Some applications benefit from using both Solid Cache and Redis:
# Intelligent cache routing
class HybridCacheStrategy
def initialize
@solid_cache = Rails.cache # Solid Cache
@redis_cache = Redis.new(url: ENV['REDIS_URL'])
end
def fetch(key, options = {})
# Route based on access patterns
if high_frequency_key?(key)
# Use Redis for high-frequency access
@redis_cache.get(key) || begin
value = yield
@redis_cache.setex(key, options[:expires_in] || 3600, value)
value
end
else
# Use Solid Cache for standard access
@solid_cache.fetch(key, options) { yield }
end
end
private
def high_frequency_key?(key)
# Keys accessed >100 times/minute use Redis
key.match?(/rate_limit|session|realtime/)
end
end
# Use cases for Redis retention
class RedisOptimalUseCases
# 1. Rate limiting (high-frequency reads/writes)
def rate_limit_check(user_id, endpoint)
key = "rate_limit:#{user_id}:#{endpoint}"
count = @redis.incr(key)
@redis.expire(key, 60) if count == 1
count <= 100 # Allow 100 requests/minute
end
# 2. Session storage (sub-millisecond access)
def session_storage
# Use Redis for session store
config.session_store :redis_store, {
servers: [ENV['REDIS_URL']],
expire_after: 90.minutes
}
end
# 3. Real-time features (pub/sub)
def realtime_notifications
@redis.publish('notifications', {
user_id: user.id,
message: 'New notification'
}.to_json)
end
end
Real-World Case Studies #
Case Study 1: Content Management Platform #
Company: Medium-sized content platform Before: Redis caching with 5GB cache After: Solid Cache with selective Redis
Migration Results: #
- Infrastructure costs: Reduced by 72% ($450/month → $125/month)
- Cache hit rate: Maintained at 85%
- Average response time: Increased by 12ms (acceptable trade-off)
- Operational complexity: Reduced significantly
- Redis usage: Kept only for real-time features (10% of previous usage)
Our Ruby on Rails development services helped this client achieve these results through careful performance analysis and strategic migration planning, ensuring zero downtime during the transition.
Case Study 2: E-commerce Application #
Company: Online retail platform Before: Memcached cluster with frequent cache invalidation issues After: Solid Cache with transactional caching
Migration Benefits: #
- Cache consistency: 100% (transactional caching eliminated race conditions)
- Deployment complexity: Reduced by removing Memcached infrastructure
- Cache warming: Automatic on deploy (database-backed)
- Cost savings: $320/month on Memcached hosting
- Developer productivity: Increased due to simpler debugging
Conclusion #
Solid Cache represents a paradigm shift in Rails caching strategy, trading marginal performance for dramatic operational simplification and cost reduction. For most Rails applications, this trade-off is overwhelmingly favorable.
Final Recommendations: #
- Migrate to Solid Cache if your cache hit rates are moderate (<10,000 reads/sec)
- Use hybrid approach for applications with mixed access patterns
- Keep Redis only for high-frequency operations and real-time features
- Monitor database impact during and after migration
- Optimize database configuration specifically for cache workloads
The future of Rails caching is database-backed, and Solid Cache provides the foundation for simpler, more cost-effective Rails deployments.
Need expert assistance with your Rails caching strategy or Solid Cache migration? Our experienced Rails team has successfully migrated applications serving millions of users, optimizing cache performance while reducing infrastructure costs by an average of 65%.
Performance benchmarks and cost estimates based on Rails 8 beta and PostgreSQL 14+. Actual results vary by application workload, database configuration, hosting provider, regional pricing, and infrastructure. Cost estimates reflect specific scenarios described in each section. Always benchmark with production-like data and obtain real pricing quotes before making caching decisions.