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_entriestable
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: <%= 256.megabytes %>
namespace: <%= 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 environmentmax_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_messagestable
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:
- Deploy the code with both migrations
- Monitor database performance
- Verify cache hit rates in your logs
- 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:
- Index Maintenance: The unique index on
keyis critical. Ensure it’s properly maintained:
# Check index health periodically
bin/rails db:analyze
- Cache Expiration: Set appropriate TTLs to prevent unbounded growth:
Rails.cache.write('key', 'value', expires_in: 1.hour)
- Database Connection Pool: If you see connection exhaustion, increase your pool size:
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 10) %>
- 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_cacheandsolid_cablegems - 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?