IMEOS
  1. Home
  2. Work
  3. Contact
  4. Blog

Im Obstgarten 7
8596 Scherzingen
Switzerland

+41 79 786 10 11
(CET/CEST, Mo-Fr, 09:00 - 18:00)
io@imeos.com

IMEOS on GitHub
Rails

Migrating from Redis to Solid Cache & Solid Cable in Rails

Oct 18, 2024
14 minutes read

Simplifying infrastructure by replacing Redis with database-backed caching and WebSockets

Redis has long been the go-to solution for caching and real-time features in Rails applications. However, managing an additional service adds operational complexity, cost, and potential points of failure. With Rails 7.2 introducing Solid Cache and Solid Cable, many apps can eliminate Redis entirely by leveraging an existing PostgreSQL database for both caching and WebSocket functionality.

This post details how to migrate from Redis to these new database-backed solutions in a production Rails application.

Why Move Away from Redis?

While Redis is excellent at what it does, it comes with trade-offs:

Operational Complexity: Another service to monitor, scale, and maintain. This means additional alerting, backups, and version management.

Cost: Managed Redis services (Heroku Redis, AWS ElastiCache) add recurring costs, especially as data grows.

Infrastructure Dependencies: Each additional service increases deployment complexity and creates more potential failure points. Connection issues, eviction policies, and memory limits all require ongoing attention.

Development Environment: Developers need Redis running locally, adding setup friction for new team members.

For many applications, especially those already running PostgreSQL, the database can handle caching and WebSocket messaging perfectly well. Modern PostgreSQL is fast, reliable, and you’re already managing it.

What are Solid Cache and Solid Cable?

Solid Cache is a database-backed implementation of ActiveSupport::Cache::Store. It stores cache entries in a PostgreSQL table instead of Redis.

Solid Cable provides a database-backed Action Cable adapter for WebSocket connections. It replaces Redis as the pub/sub backend for broadcasting messages across multiple server instances.

Both are official Rails libraries maintained by the Rails core team, introduced alongside Rails 8 but compatible with Rails 7.2+.

The Migration Process

Step 1: Add the Gems

Replace redis with the Solid gems in your Gemfile:

# Remove:
# gem 'redis', '~> 5.3.0'

# Add:
gem 'solid_cache', '~> 1.0'
gem 'solid_cable', '~> 3.0'

Run bundle install to update dependencies.

Step 2: Install and Configure Solid Cache

Generate the Solid Cache configuration and migration:

bin/rails solid_cache:install

This creates:

  • config/cache.yml - Cache configuration
  • A migration file for the solid_cache_entries table

The migration creates a table to store cache entries:

class CreateSolidCacheTable < ActiveRecord::Migration[7.2]
  def change
    create_table :solid_cache_entries do |t|
      t.binary :key, limit: 1024, null: false
      t.binary :value, limit: 536_870_912, null: false # ~512MB max
      t.datetime :created_at, null: false
      
      t.index :key, unique: true
    end
  end
end

Configure config/cache.yml:

default: &default
  store_options:
    max_size: <&percnt;&#61; 256.megabytes %>
    namespace: <&percnt;&#61; Rails.env %>

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

Key configuration options:

  • max_size: Maximum size of individual cache entries (default 1MB, we increased to 256MB for complex objects)
  • namespace: Isolates cache entries by environment
  • max_age: Optional expiration for all cache entries (useful for retention policies)

Step 3: Install and Configure Solid Cable

Generate Solid Cable configuration and migration:

bin/rails solid_cable:install

This creates:

  • Updates to config/cable.yml
  • A migration file for the solid_cable_messages table

The migration:

class CreateSolidCableMessages < ActiveRecord::Migration[7.2]
  def change
    create_table :solid_cable_messages do |t|
      t.binary :channel, limit: 1024, null: false
      t.binary :payload, limit: 536_870_912, null: false
      t.datetime :created_at, null: false
      
      t.index :channel
      t.index :created_at
    end
  end
end

Update config/cable.yml:

development:
  adapter: async  # Single-process, no database needed

test:
  adapter: test

production:
  adapter: solid_cable
  polling_interval: 0.1.seconds
  message_retention: 1.day

Key configuration:

  • polling_interval: How often to check for new messages (balance between latency and database load)
  • message_retention: How long to keep messages before cleanup (1 day is usually sufficient)

Step 4: Update Environment Configuration

Replace Redis cache store configuration in your environment files.

config/environments/production.rb:

# Before:
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }

# After:
config.cache_store = :solid_cache_store

config/environments/development.rb:

# Before:
config.cache_store = :redis_cache_store

# After:
config.cache_store = :solid_cache_store

Step 5: Remove Redis Dependencies

Clean up Redis-related code throughout your application:

Remove Redis initializer (config/initializers/redis.rb):

# Delete this file entirely
# Redis.new(url: ENV['REDIS_URL'], ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE })

Update health checks if you’re checking Redis connectivity:

# config/allgood.rb or similar

# Remove:
check 'Can connect to Redis' do
  make_sure Redis.new.ping == 'PONG'
end

# Keep general cache check (works with any cache store):
check 'Cache is accessible and functioning' do
  Rails.cache.write('health_check_test', 'ok')
  make_sure Rails.cache.read('health_check_test') == 'ok'
end

Update Procfile.dev if running Redis locally:

# Remove the redis line:
# redis: redis-server

Step 6: Run Migrations

Apply the database changes:

bin/rails db:migrate

This creates the solid_cache_entries and solid_cable_messages tables in your database.

Step 7: Test Thoroughly

Before deploying, verify everything works:

Test caching:

# In rails console
Rails.cache.write('test_key', 'test_value')
Rails.cache.read('test_key') # Should return 'test_value'
Rails.cache.delete('test_key')

Test Action Cable (if you use WebSockets):

  • Open multiple browser windows
  • Trigger a broadcast
  • Verify all clients receive the message

Run your test suite:

bin/rails test
bin/rails test:system

Step 8: Deploy

Deploy to your staging environment first:

  1. Deploy the code with both migrations
  2. Monitor database performance
  3. Verify cache hit rates in your logs
  4. Test WebSocket functionality if applicable

Once stable, deploy to production.

Step 9: Remove Redis from Infrastructure

After confirming everything works:

Heroku:

heroku addons:destroy heroku-redis -a your-app-name

Other platforms:

  • Terminate Redis instances
  • Remove Redis from environment variables
  • Update infrastructure-as-code configurations

Performance Considerations

Database Load: Cache reads/writes now hit your database. For most applications, this is fine. PostgreSQL is fast, and you’re likely not near capacity.

Monitor These Metrics:

  • Database connection pool usage
  • Query performance (cache reads should be very fast)
  • Table size growth for solid_cache_entries

Optimization Tips:

  1. Index Maintenance: The unique index on key is critical. Ensure it’s properly maintained:
# Check index health periodically
bin/rails db:analyze
  1. Cache Expiration: Set appropriate TTLs to prevent unbounded growth:
Rails.cache.write('key', 'value', expires_in: 1.hour)
  1. Database Connection Pool: If you see connection exhaustion, increase your pool size:
# config/database.yml
production:
  pool: <%= ENV.fetch("RAILS_MAX_THREADS", 10) %>
  1. Cleanup Job: Solid Cable includes automatic cleanup for old messages based on message_retention, but you may want to add explicit cleanup for cache entries:
# lib/tasks/cache_cleanup.rake
namespace :cache do
  desc 'Clean up old cache entries'
  task cleanup: :environment do
    SolidCache::Entry.where('created_at < ?', 7.days.ago).delete_all
  end
end

Real-World Results

After migrating several production applications:

Cost Savings: Eliminated Redis addon cost.

Simplified Operations: One less service to monitor, upgrade, and troubleshoot. No more Redis eviction warnings or memory limit concerns.

Development Setup: New developers can skip Redis installation. bin/setup now just works with PostgreSQL alone.

Performance: No noticeable difference. Database CPU usage increased by ~2%, well within capacity. Cache hit rates remained consistent.

WebSocket Reliability: Solid Cable proved equally reliable as Redis for our Action Cable usage (user notifications and live updates).

When NOT to Replace Redis

Solid Cache and Solid Cable aren’t ideal for every scenario:

High Cache Volume: If you’re caching gigabytes of data or doing millions of cache operations per second, Redis is more appropriate.

Very Low Latency Requirements: Redis is faster than database queries. If you need sub-millisecond cache responses consistently, stick with Redis.

Redis-Specific Features: If you use Redis data structures (sorted sets, pub/sub, streams) beyond basic caching, you still need Redis.

Separate Scaling: If your cache layer needs to scale independently from your database, Redis offers that flexibility.

Migration Checklist

  • Add solid_cache and solid_cable gems
  • Run install generators
  • Update environment configurations
  • Remove Redis initializers and health checks
  • Run database migrations
  • Test caching functionality
  • Test WebSocket functionality (if applicable)
  • Deploy to staging
  • Monitor database performance
  • Deploy to production
  • Monitor for 48 hours
  • Remove Redis from infrastructure
  • Update documentation

Conclusion

For many Rails applications, Redis is overkill. If you’re using it primarily for caching and Action Cable, Solid Cache and Solid Cable provide simpler, cheaper alternatives with minimal trade-offs.

The migration is straightforward, the performance impact is negligible for most apps, and the operational benefits are immediate. By leveraging your existing PostgreSQL database, you reduce complexity while maintaining the functionality you need.

Modern PostgreSQL is powerful enough to handle caching and real-time messaging alongside your application data. Why manage two data stores when one will do?

Further Reading

  • Solid Cache Documentation
  • Solid Cable Documentation
  • Rails 8 Release Notes

  • Privacy
  • Imprint
IMEOS