Solid Cache — Replacing Redis With Your Database, and When Not To
Every Rails app reaches the point where it needs a cache store, and the reflex answer has been Redis for a decade. Solid Cache challenges that by putting the cache in the database you already run — no extra service, no extra credentials, no extra thing to page you at 2am. The trade it makes is real and worth understanding before you rip Redis out, because “the database is slower than memory” is both true and mostly irrelevant.
The Premise
Redis is fast because it’s in RAM. That speed caps your cache at whatever RAM you’re willing to pay for — typically a few gigabytes — which means aggressive eviction and a lower hit rate.
Solid Cache writes to disk, which is roughly an order of magnitude slower per read. But modern NVMe reads land in the low hundreds of microseconds, and disk is cheap enough that you can hold a much larger working set. A slower cache with a 95% hit rate frequently beats a faster cache with a 60% hit rate, because a miss costs you the full uncached query.
That’s the whole argument: trade per-operation latency for hit rate and operational simplicity.
Setting It Up
Setup:
bin/rails solid_cache:install
bin/rails db:migrate
The installer creates the migration and points the production cache store at it.
Example:
# config/environments/production.rb
config.cache_store = :solid_cache_store
Migration:
create_table :solid_cache_entries do |t|
t.binary :key, null: false, limit: 1024
t.binary :value, null: false, limit: 536_870_912
t.datetime :created_at, null: false
t.integer :key_hash, null: false, limit: 8
t.integer :byte_size, null: false, limit: 4
t.index :key_hash, unique: true
t.index [:key_hash, :byte_size]
t.index :byte_size
end
One table. Lookups go through key_hash rather than the key itself, which keeps the index small and fixed-width regardless of how long your cache keys get.
Give It Its Own Database
This is the configuration that matters most, and the installer nudges you toward it for good reason. Cache writes are high-volume, low-value churn — you don’t want them in the same database as your orders, competing for connections and bloating your backups.
Example:
# config/database.yml
production:
primary:
<<: *default
database: myapp_production
cache:
<<: *default
database: myapp_production_cache
migrations_paths: db/cache_migrate
# config/environments/production.rb
config.solid_cache.connects_to = { database: { writing: :cache } }
config.cache_store = :solid_cache_store
A separate database means you can back it up differently (or not at all — it’s a cache), size it independently, and drop the whole thing without touching application data.
Configuring Size and Eviction
Solid Cache doesn’t evict on a timer. It tracks total size and trims the oldest entries once you exceed the limit.
Example:
# config/cache.yml
production:
database: cache
store_options:
max_age: <%= 30.days.to_i %>
max_size: <%= 50.gigabytes %>
namespace: <%= Rails.env %>
max_size— the target total. Trimming is probabilistic and happens during writes, so the actual size hovers around this figure rather than hitting it exactly.max_age— a hard upper bound on entry age regardless of size.namespace— essential if two environments share a cache database.
Fifty gigabytes of cache costs a few dollars a month on disk. The equivalent in Redis RAM does not, and that gap is the entire value proposition.
Encryption
Because cache entries sit in your database and end up in database backups, encrypting them is more relevant than it was for an ephemeral in-memory store.
Example:
production:
database: cache
store_options:
encrypt: true
This uses Active Record Encryption, so it needs the standard key setup in your credentials. It adds CPU on read and write — measure it before enabling on a hot path, and consider whether the right answer is instead to not cache sensitive values.
Where Redis Still Wins
Being honest about this matters, because Solid Cache is a cache store and Redis is several other things as well.
- Sub-millisecond p99 requirements. If your cache read is inside a loop that runs a thousand times per request, the microsecond difference compounds. Fix the loop first, but if you can’t, RAM wins.
- Data structures. Redis sorted sets, HyperLogLog, streams, and pub/sub have no equivalent here. Solid Cache is a key-value store and nothing more.
- Rate limiting and distributed locks. These need atomic operations Redis provides natively.
Rails.cachecan’t do a correct distributed lock, and neither can Solid Cache. - Action Cable at scale. The Redis adapter is battle-tested; Solid Cable is the analogous replacement, and it’s a separate decision.
- A database already at its IOPS ceiling. Adding cache write volume to a struggling database makes both worse. This is the main reason to say no.
The honest framing: Solid Cache replaces Rails.cache. It does not replace Redis in an application that uses Redis for six things.
Migrating Without a Cold Cache
Switching stores empties the cache, and a cold cache on a busy app can cause a thundering herd against the database — which is exactly what the cache existed to prevent.
Warm it before cutover, or stagger the switch:
Example:
# config/environments/production.rb
config.cache_store = if ENV["CACHE_BACKEND"] == "solid"
:solid_cache_store
else
:redis_cache_store, { url: ENV["REDIS_URL"] }
end
Flip the environment variable on one instance, watch its error rate and database load for a day, then roll the rest. Keep Redis running until you’ve been on Solid Cache through a full traffic peak.
For the fragment caches that matter most, a warming job is cheap insurance:
class WarmCacheJob < ApplicationJob
def perform
Product.featured.find_each do |product|
Rails.cache.fetch(["product-summary", product]) { render_summary(product) }
end
end
end
Pro-Tip: Before deciding between Solid Cache and Redis, measure your actual hit rate and working set — the decision turns entirely on those two numbers and almost nobody knows them. Instrument it: subscribe to
cache_read.active_supportand count hits versus misses over a representative day, and check your Redisused_memoryagainstmaxmemoryplus theevicted_keyscounter. If evictions are non-zero, your cache is too small and Solid Cache’s cheap capacity is a direct win. If evictions are zero and your working set fits comfortably in the RAM you’re already paying for, the case is much weaker and you’re mostly buying operational simplicity. Ten minutes withRails.cache.statsand a notification subscriber tells you more than any benchmark comparison you’ll read.
Instrumenting It
Example:
# config/initializers/cache_instrumentation.rb
ActiveSupport::Notifications.subscribe(/cache_(read|write|delete)\.active_support/) do |event|
Rails.logger.info(
"cache op=#{event.name.split('.').first} " \
"hit=#{event.payload[:hit]} " \
"key=#{event.payload[:key].to_s.first(60)} " \
"duration_ms=#{event.duration.round(2)}"
)
end
Aggregate hit=true versus hit=false and you have the number that justifies or kills the whole migration.
Conclusion
Solid Cache trades per-read latency for capacity and one fewer service to operate, and for most Rails applications that trade is favourable — a large disk-backed cache with a high hit rate beats a small RAM-backed cache that evicts constantly. Give it its own database so cache churn stays out of your backups and connection pool, set max_size generously because disk is cheap, and roll it out behind an environment variable so you can watch a real traffic peak before committing. Keep Redis if you’re using it for locks, rate limiting, pub/sub, or data structures — Solid Cache replaces Rails.cache, not Redis itself.
FAQs
Q1: Does Solid Cache work with MySQL and SQLite?
Yes — PostgreSQL, MySQL, and SQLite are all supported. SQLite is a reasonable choice for single-server deployments and is part of what makes the Rails 8 single-box story work.
Q2: What happens when the cache database is unavailable?
Cache reads return nil and writes are dropped, same as any failing cache store — the app degrades to uncached rather than erroring. Confirm this by testing it, since a misconfigured connection pool can turn a cache outage into an application outage.
Q3: How does trimming affect write performance?
Trimming runs probabilistically during writes, deleting a small batch of the oldest rows. The per-write cost is small and amortised, but on a database near its IOPS limit the extra delete volume is worth accounting for.
Q4: Can I use Solid Cache in development?
Yes, though :memory_store is simpler and faster for local work. Use Solid Cache in development only if you’re specifically testing cache behaviour that depends on the store.
Q5: Is it a drop-in replacement for redis_cache_store?
For the Rails.cache API — fetch, read, write, delete, increment — yes. Code reaching for Rails.cache.redis or issuing raw Redis commands will break, and that’s the code to grep for before switching.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀