Sijin T V
Sijin T V A passionate Software Engineer who contributes to the wonders happenning on the internet

The Rails 8 Solid Suite: Moving to a Zero-Dependency Stack

Rails 8 (November 2024) did something bold: it made the database the default home for everything that used to need Redis. Solid Queue for background jobs, Solid Cache for the fragment cache, Solid Cable for ActionCable — all three backed by plain SQL tables. A year on, this stack has been through enough production deployments that the marketing question (“can you really drop Redis?”) has been replaced by an engineering one (“what exactly does it cost you?”). We’ve run all three in production since early 2025, and the answer is: less than people fear, more than the docs admit, and it’s all concentrated in a few measurable places.

What the suite actually is

Three gems, one principle: store the state in rows and let SQL do the coordination.

  • Solid Queue runs Sidekiq-style workers but with a Postgres/MySQL/SQLite queue instead of Redis lists. Jobs, scheduled jobs, and recurring jobs are rows; workers claim work with FOR UPDATE SKIP LOCKED, so concurrent workers don’t fight over a record.
  • Solid Cache is a FIFO disk cache — write batches flushed asynchronously, reads as batched key_hash IN (...) queries, eviction by ID range. Sub-millisecond on a warm buffer pool.
  • Solid Cable polls a solid_cable_messages table for broadcasts instead of maintaining pub/sub connections.

The configuration is deliberately boring:

1
2
3
4
# Gemfile — Rails 8 defaults
gem "solid_queue"
gem "solid_cache"
gem "solid_cable"
1
2
3
4
5
6
# config/cache.yml
production:
  database: cache
  store_options:
    max_size: 100.gigabytes
    max_age: 2.weeks

All three point at separate databases declared in config/database.yml (primary, cache, cable, queue), each with its own migrations_paths. The suite’s whole premise is “one engine, four databases, zero other stateful services,” and on that premise it delivers: a single Postgres instance running a Kamal-deployed Rails 8 app is a genuinely one-server architecture, and Rails 8.1 (September 2025) tidied the config surfaces so the three gems read from one place.

The costs, measured

The honest ledger after a year at around 2M jobs a week on one 8-core Postgres:

  • Job latency is polling latency. Solid Queue’s default polling_interval is 5 seconds, so an empty queue means jobs wait up to that. We dropped it to 1 second; that’s 5x the poll queries for latency nobody can feel. Measure, then tune — don’t inherit the default blindly.
  • Vacuum churn is the real operational cost. Solid Queue’s job tables churn hard (inserts, claims, deletes per job), and the cache and cable tables delete constantly. We set autovacuum_vacuum_scale_factor down and autovacuum_vacuum_threshold up on those tables, and we watch n_dead_tup like a critical metric. Bloat is how this stack dies quietly.
  • Transaction stress moves from Redis to Postgres. Solid Queue uses advisory locks and SKIP LOCKED so workers don’t deadlock each other, but a spike of concurrent enqueues still shows up as table contention. At our scale it’s invisible; at 10x it wouldn’t be.

Where the suite still loses

Three workloads kept Redis (or would again):

  • High-frequency pub/sub fan-out. Solid Cable polled at 100 ms matches Redis for a few hundred sockets but degrades badly past a few thousand concurrent connections. That’s AnyCable territory.
  • Sub-second job latency. If a job must run in 500 ms, a polling queue is the wrong tool regardless of tuning.
  • Redis-native data structures. Rate-limiting counters, sorted-set leaderboards, INCR-heavy metrics — these aren’t jobs or caches, and reimplementing them in SQL tables is writing your own Redis.

Production lessons

  • Separate databases, not just connections. We started with Solid Queue on the primary DB “to save ops.” Vacuum churn and SKIP LOCKED contention bled into transactional work. Moving it to its own DB was the single best change.
  • Set polling_interval deliberately. The default optimizes for idle cost, not latency. If your recurring jobs or heartbeat endpoints feel sluggish, this is the first knob.
  • Watch n_dead_tup and connection counts, not just CPU. The suite’s failure modes are bloat and pool exhaustion, both visible before they’re fatal.
  • Prefer expiry_method: :job for Solid Cache on multi-process deployments, or you’ll have one expiry thread per process.
  • Plan the Redis exit and the Redis stay. Deleting Redis for a hobby app or an internal tool is a genuine win — one less thing to back up, patch, and babysit. Keeping it because you use its data structures is correct too. What’s no longer defensible is running Redis “because Rails always did.”

The Solid Suite’s quiet achievement is making the zero-dependency default good enough that “one Postgres, one box” is a real architecture, not a demo. Know what it costs you, measure the churn, and it will carry a surprising amount of production traffic — just don’t ask it to be Redis.

comments powered by Disqus