I recently wanted to use Solid Cache for a new MVP I’m building out in Ruby on Rails. One thing I wanted was for this to all be deployed to a PaaS and in this case I was using Heroku. While I know Rails 8 pushes hard for Kamal and rolling your own deployment, for a lot of early projects it’s nice to just have all the DevOps and CI/CD taken care of for you.
This creates a problem when it comes to Solid Cache. Rails recommends running these with SQLite and in fact I have a production application using SQLite for everything that works amazing. However, Heroku is an ephemeral application server and as such, wipes out your SQLite stores on every deployment.
Since this was an MVP I really just wanted to manage one database rather than introduce Redis or another instance of Postgres. Aft…
I recently wanted to use Solid Cache for a new MVP I’m building out in Ruby on Rails. One thing I wanted was for this to all be deployed to a PaaS and in this case I was using Heroku. While I know Rails 8 pushes hard for Kamal and rolling your own deployment, for a lot of early projects it’s nice to just have all the DevOps and CI/CD taken care of for you.
This creates a problem when it comes to Solid Cache. Rails recommends running these with SQLite and in fact I have a production application using SQLite for everything that works amazing. However, Heroku is an ephemeral application server and as such, wipes out your SQLite stores on every deployment.
Since this was an MVP I really just wanted to manage one database rather than introduce Redis or another instance of Postgres. After a lot of failed attempts and much googling, this was the solution I came up with.
The Problem
After deploying the Rails 8 application to Heroku, I encountered this error when trying to use rate limiting:
PG::UndefinedTable (ERROR: relation "solid_cache_entries" does not exist)
This occurred because Rails 8’s rate limiting feature depends on Rails.cache, which in production is configured to use Solid Cache by default. However, the solid_cache_entries table didn’t exist in our database.
This worked locally for me because in development Rails uses an in memory store so no database was required. It wasn’t until deployment that I was able to see the error.
Understanding Solid Cache
Rails 8 introduces the “Solid” stack as default infrastructure:
- Solid Cache - Database-backed cache store (replaces Redis/Memcached)
- Solid Queue - Database-backed job queue (replaces Sidekiq/Resque)
- Solid Cable - Database-backed Action Cable (replaces Redis for WebSockets)
By default, Rails 8 expects these to use separate databases. The solid_cache:install generator creates:
config/cache.yml- Cache configurationdb/cache_schema.rb- Schema file (NOT a migration)- Configuration pointing to a separate
cachedatabase
Why Use a Single Database on Heroku?
For our MVP, I chose to use a single PostgreSQL database for several reasons:
Cost and Simplicity
- Heroku provides one DATABASE_URL - Additional databases cost extra
- Simpler architecture - Fewer moving parts during initial development
- Easier to manage - Single database connection, single backup strategy
Future Scalability Options
When you outgrow this setup, you have clear upgrade paths:
- Redis - Better performance for high-traffic apps, separate caching layer
- Separate PostgreSQL database - Isolate cache from primary data
- Managed cache service - Heroku Redis, AWS ElastiCache, etc.
Why Not SQLite for Cache on Heroku?
While Solid Cache supports SQLite, Heroku’s filesystem is ephemeral:
- Files are wiped on dyno restart (at least once per 24 hours)
- Deployments create new dynos with fresh filesystems
- You’d lose all cached data frequently
SQLite-backed Solid Cache works great for:
- Single-server VPS deployments (Kamal, Hetzner, DigitalOcean Droplets)
- Containerized apps with persistent volumes
- Development/staging environments
But for Heroku and similar PaaS platforms, use PostgreSQL or Redis for caching.
What I Tried (And What Didn’t Work)
Attempt 1: Running the Generator
bin/rails solid_cache:install
Result: Created cache_schema.rb but no migration file. Changed cache.yml to point to a separate cache database that doesn’t exist on Heroku.
Attempt 2: Official Multi-Database Setup
Following the official Rails guides, I configured database.yml with separate database entries:
production:
primary:
url: <%= ENV["DATABASE_URL"] %>
cache:
url: <%= ENV["DATABASE_URL"] %> # Same database, different connection
And cache.yml:
production:
database: cache
Result: The cache_schema.rb file wasn’t loaded by db:migrate or db:prepare. Rails expected separate databases with separate schema files.
Attempt 3: Using db:prepare
Ran bin/rails db:prepare hoping it would load all schema files.
Result: Only loaded db/schema.rb (main migrations), ignored db/cache_schema.rb.
The Solution: Migration-Based Approach
After researching (including this Reddit thread), I found the working solution for single-database Heroku deployments.
Step 1: Configure cache.yml for Single Database
Remove the database: configuration from production in config/cache.yml:
# config/cache.yml
default: &default
store_options:
max_size: <%= 256.megabytes %>
namespace: <%= Rails.env %>
development:
<<: *default
test:
<<: *default
production:
<<: *default # No database: specified - uses primary connection
Important: According to the Solid Cache README, when you omit database, databases, or connects_to settings, Solid Cache automatically uses the ActiveRecord::Base connection pool (your primary database).
Step 2: Create a Migration for the Cache Table
Generate a migration to create the solid_cache_entries table:
bin/rails generate migration CreateSolidCacheEntries --database=cache
The --database=cache flag keeps the migration organized (though it still runs against the primary database in our single-DB setup).
Step 3: Copy Table Definition from cache_schema.rb
Update the generated migration with the exact table structure from db/cache_schema.rb:
# db/migrate/YYYYMMDDHHMMSS_create_solid_cache_entries.rb
class CreateSolidCacheEntries < ActiveRecord::Migration[8.1]
def change
create_table :solid_cache_entries do |t|
t.binary :key, limit: 1024, null: false
t.binary :value, limit: 536870912, null: false
t.datetime :created_at, null: false
t.integer :key_hash, limit: 8, null: false
t.integer :byte_size, limit: 4, null: false
t.index :byte_size
t.index [:key_hash, :byte_size]
t.index :key_hash, unique: true
end
end
end
Step 4: Run Migration Locally
bin/rails db:migrate
Verify the table was created:
bin/rails runner "puts ActiveRecord::Base.connection.table_exists?('solid_cache_entries')"
# Should output: true
Step 5: Keep Development Simple
Leave development environment using :memory_store in config/environments/development.rb:
config.cache_store = :memory_store
This is the Rails convention and keeps development simple. Production uses Solid Cache, development uses in-memory caching.
Step 6: Deploy to Heroku
Your existing Procfile with the release phase will handle the migration:
release: bundle exec rails db:migrate
web: bundle exec puma -C config/puma.rb
Deploy and the migration runs automatically during the release phase.
Verification
After deployment, verify Solid Cache is working:
- Check Heroku logs during release phase - should see migration run
- Test rate limiting - Try to trigger rate limits on login endpoints
- Check cache in Rails console:
# On Heroku
heroku run rails console
# Test cache
Rails.cache.write('test_key', 'test_value')
Rails.cache.read('test_key') # Should return 'test_value'
# Check table
SolidCache::Entry.count # Should be > 0 if cache is working
Cache Expiration and Cleanup
Solid Cache includes automatic cleanup to prevent indefinite growth. Our configuration uses both size and age-based expiration:
# config/cache.yml
default: &default
store_options:
max_age: <%= 30.days.to_i %> # Delete entries older than 30 days
max_size: <%= 256.megabytes %> # Delete oldest when exceeds 256MB
namespace: <%= Rails.env %>
How Automatic Cleanup Works
Solid Cache uses a write-triggered background thread (not a separate job system):
- Write tracking - Every cache write increments an internal counter
- Automatic activation - After ~50 writes, cleanup runs on a background thread
- Cleanup logic:
- If
max_sizeexceeded → Delete oldest entries (LRU eviction) - If
max_ageset and size OK → Delete entries older than max_age - Deletes in batches of 100 entries (
expiry_batch_size)
- SQL-based deletion - Runs standard SQL DELETE queries:
DELETE FROM solid_cache_entries
WHERE created_at < NOW() - INTERVAL '30 days'
LIMIT 100
Important Characteristics
- ✅ No Solid Queue required - Uses built-in Ruby threads, not Active Job
- ✅ No cron jobs needed - Self-managing and automatic
- ✅ Database-agnostic - Pure SQL, works with any ActiveRecord adapter
- ✅ Efficient - Background thread idles when cache isn’t being written to
- ⚠️ Write-dependent - Cleanup only triggers when cache receives writes
For rate limiting (which writes on every login attempt), this mechanism works perfectly and requires no additional infrastructure.
Why 30 Days for Rate Limiting?
Rate limiting data is inherently short-lived:
- Rate limit windows are 3 minutes
- Session data expires in days, not months
- Old cache entries from expired sessions serve no purpose
30 days is generous for this use case and prevents cache bloat while maintaining safety margins.
Key Takeaways
- Rails 8’s default Solid Stack assumes multi-database setup - This doesn’t match Heroku’s single DATABASE_URL model
- Schema files aren’t migrations -
db/cache_schema.rbwon’t be loaded bydb:migrate - Omitting
database:in cache.yml uses the primary connection - This is the key for single-database setups - Create a regular migration - Convert the schema file to a migration for single-database deployments
- SQLite doesn’t work on ephemeral filesystems - Use PostgreSQL or Redis for caching on Heroku
- Development can use :memory_store - No need to complicate local development
- Automatic cleanup is built-in - Solid Cache handles expiration via background threads, no Solid Queue or cron jobs required
Future Migration Path
When your app scales and you need better cache performance:
- Add Heroku Redis (~$15/month for hobby tier)
- Update production.rb:
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
- Remove Solid Cache dependency if desired, or keep for other purposes
The migration is straightforward and won’t require code changes beyond configuration.