Rails 8 Solid Cache: Database-Backed Cache Store

Caching in Rails has traditionally meant choosing between Redis or Memcached. Both are fast but expensive when we need large caches. Memory costs add up quickly.

Rails 8 introduces Solid Cache as the default production cache store. It stores cache entries in the database, leveraging modern SSD performance to provide larger caches at significantly lower costs.

In this post, we will explore how Solid Cache works, how to configure it for production, and important tradeoffs to consider.

The Case for Database Backed Caching

Traditional memory based caches have a fundamental limitation: RAM is expensive. A 10GB Redis cache costs significantly more than 10GB of SSD storage.

Modern NVMe SSDs have changed the performance equation. Read latencies are now measured in microseconds, making disk based caching viable for most use cases.

Solid Cache takes advantage of this shift. We can now maintain caches measured in hundreds of gigabytes at a fraction of the cost of equivalent memory based solutions.

Installation

Solid Cache is preconfigured in new Rails 8 applications. For existing applications on Rails 7.1 or later:

bundle add solid_cache
bin/rails solid_cache:install

This creates:

  • config/cache.yml for cache configuration
  • db/cache_schema.rb for the database schema

Database Configuration

Like Solid Queue, Solid Cache works best with a dedicated database. Here is a SQLite configuration:

# config/database.yml



production:

  primary:

    <<: *default

    database: storage/production.sqlite3

  cache:

    <<: *default

    database: storage/production_cache.sqlite3

    migrations_paths: db/cache_migrate

For PostgreSQL:

# config/database.yml



production:

  primary:

    <<: *default

    database: myapp_production

  cache:

    <<: *default

    database: myapp_cache_production

    migrations_paths: db/cache_migrate

Run the setup:

bin/rails db:prepare

Configuration Options

Configure Solid Cache in config/cache.yml:

# config/cache.yml



production:

  database: cache

  store_options:

    max_age: 1.week

    max_size: 256.megabytes

    namespace: myapp_prod

    compress: true

    compress_threshold: 1.kilobyte

Key options explained:

  • max_age: Maximum time entries stay in cache before expiration
  • max_size: Maximum total cache size (triggers cleanup when exceeded)
  • max_entries: Alternative to max_size, limits by entry count
  • compress: Enable compression for large values
  • compress_threshold: Minimum size before compression kicks in

Real World Usage Examples

Fragment Caching

Solid Cache works seamlessly with Rails fragment caching:

<%# app/views/products/show.html.erb %>

<% cache @product do %>
  <div class="product-details">
    <h1><%= @product.name %></h1>
    <p><%= @product.description %></p>
    <%= render @product.reviews %>
  </div>
<% end %>

Low Level Caching

For complex data that is expensive to compute:

# app/models/dashboard.rb


class Dashboard
  def self.metrics_for(user)
    Rails.cache.fetch("dashboard/#{user.id}/metrics", expires_in: 15.minutes) do
      {
        total_orders: user.orders.count,
        revenue: user.orders.sum(:total),
        average_order: user.orders.average(:total),
        recent_activity: user.activities.recent.to_a
      }
    end
  end
end

Russian Doll Caching

Solid Cache handles nested caching efficiently:

<%# app/views/categories/show.html.erb %>

<% cache @category do %>
  <h1><%= @category.name %></h1>

  <% @category.products.each do |product| %>
    <% cache product do %>
      <%= render product %>
    <% end %>
  <% end %>
<% end %>

When a product updates, only its fragment expires. The category fragment rebuilds with the new product cache.

API Response Caching

Cache expensive API responses:

# app/controllers/api/reports_controller.rb


class Api::ReportsController < ApplicationController
  def sales
    data = Rails.cache.fetch("api/reports/sales/#{Date.current}", expires_in: 1.hour) do
      SalesReport.generate(
        start_date: 30.days.ago,
        end_date: Date.current
      )
    end

    render json: data
  end
end

Cache Expiration Strategy

Solid Cache uses a FIFO (First In First Out) expiration strategy. This differs from LRU (Least Recently Used) caches like Redis.

The tradeoff is simpler implementation and better database performance. With large cache sizes enabled by Solid Cache, the difference is negligible for most applications.

Expiration happens automatically when:

  • Entries exceed max_age
  • Total size exceeds max_size
  • Total entries exceed max_entries

Configure the expiration batch size:

# config/cache.yml



production:

  store_options:

    expiry_batch_size: 100

    expiry_method: :job

Setting expiry_method: :job runs cleanup in a background job instead of inline during writes. This improves write performance for high throughput applications.

Sharding for Scale

For very large caches, Solid Cache supports sharding across multiple databases:

# config/database.yml



production:

  cache_shard_1:

    <<: *default

    database: myapp_cache_1

  cache_shard_2:

    <<: *default

    database: myapp_cache_2
# config/environments/production.rb


config.solid_cache.connects_to = {
  shards: {
    shard_1: { writing: :cache_shard_1 },
    shard_2: { writing: :cache_shard_2 }
  }
}
# config/cache.yml



production:

  store_options:

    cluster:

      - shards: [shard_1, shard_2]

Solid Cache uses consistent hashing to distribute keys across shards.

Encryption

For sensitive cached data, enable encryption:

# config/cache.yml



production:

  store_options:

    encrypt: true

This requires Active Record Encryption to be configured in the application.

Caveats and Considerations

Latency Compared to Redis

Solid Cache is slower than Redis for individual operations. Typical latencies:

  • Redis: 0.1 to 0.5 milliseconds
  • Solid Cache: 1 to 5 milliseconds

For most web applications, this difference is imperceptible. However, if the application makes hundreds of cache calls per request, the latency adds up.

Profile the application before switching.

Database Connection Pool

Solid Cache uses database connections from the pool. Ensure adequate connections:

# config/database.yml



production:

  cache:

    <<: *default

    database: myapp_cache

    pool: <%= ENV.fetch("CACHE_DB_POOL", 10) %>

Write Amplification

Every cache write is a database write. High write volumes can impact database performance and increase storage I/O.

Monitor the database metrics after enabling Solid Cache.

FIFO vs LRU

The FIFO expiration strategy means frequently accessed items can be evicted before rarely accessed ones. This is usually fine with large caches but can cause unexpected cache misses if the cache size is constrained.

No Pub/Sub

Unlike Redis, Solid Cache does not support pub/sub patterns. If we use Redis for both caching and real time features, we may still need Redis for the latter.

Backup Considerations

The cache is now part of the database. Consider whether we need to include it in backups. For most applications, cache data is regenerable and can be excluded from backups to save space.

Cold Start Performance

After a deployment or restart, the cache is empty. This can cause a thundering herd problem where many requests simultaneously try to populate the cache.

Implement cache warming for critical data:

# lib/tasks/cache.rake


namespace :cache do
  desc "Warm critical caches"
  task warm: :environment do
    Product.featured.find_each do |product|
      Rails.cache.fetch("products/#{product.id}") { product.to_cache_hash }
    end

    Category.active.find_each do |category|
      Rails.cache.fetch("categories/#{category.id}") { category.to_cache_hash }
    end
  end
end

Migration from Redis

Migrating from Redis cache is straightforward:

  1. Install Solid Cache
  2. Configure the cache database
  3. Update environment configuration
  4. Deploy and monitor
# config/environments/production.rb


# Before

config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }

# After

config.cache_store = :solid_cache_store

The cache will be empty after migration. Plan for cache warming or accept temporary performance degradation.

When to Use Solid Cache

Solid Cache is ideal when:

  • We want to reduce infrastructure complexity
  • We need large caches (tens or hundreds of gigabytes)
  • Cost is a concern
  • Individual cache latency is not critical

Consider alternatives when:

  • We need sub millisecond cache latency
  • The application makes many cache calls per request
  • We already have Redis for other features
  • We need pub/sub or other Redis specific features

Performance Tuning

Compression

Enable compression for large values:

# config/cache.yml



production:

  store_options:

    compress: true

    compress_threshold: 1.kilobyte

This reduces storage and I/O at the cost of CPU.

Connection Pooling

Use a connection pool sized for the workload:

# config/database.yml



production:

  cache:

    pool: <%= ENV.fetch("CACHE_DB_POOL", 20) %>

Index Optimization

Solid Cache creates appropriate indexes automatically. For very large caches, monitor query performance and consider database specific optimizations.

Conclusion

Solid Cache brings database backed caching to Rails 8, enabling larger caches at lower costs. It integrates seamlessly with existing Rails caching patterns and requires minimal configuration changes.

The tradeoffs around latency and write amplification are acceptable for most applications. Profile the specific workload to ensure Solid Cache meets the needs.

Check the official Solid Cache repository for the latest documentation and configuration options.

Need expert help with Rails performance?

Saeloun is a Rails Foundation Contributing Member helping teams modernize, upgrade, scale, and maintain production Rails applications.

Our Expertise

  • Rails contributors
  • 500+ Technical Articles
  • Production Rails consulting
  • Performance Optimization

Services

  • Rails application development
  • Code Audits
  • Rails upgrades
  • Team Augmentation

Need help on your Ruby on Rails or React project?

Join Our Newsletter