Rails 8 in production: the Solid stack, six months on
When Rails 8 shipped, I was enthusiastic about the no-Redis 'Solid' stack and the one-person-framework thesis. Now I've run it in production for six months. Here's the honest retrospective: where it delivered, where the rough edges were, and whether the simplification held up under real load.
When Rails 8 shipped, I wrote enthusiastically about its thesis: a collapsed, no-Redis stack — Solid
Queue for jobs, Solid Cache for caching, all in Postgres — plus Kamal for deployment, adding up to a
complete path from rails new to production that one person could build and run. Enthusiasm at release is
cheap, though. The honest test is living with it, so here’s the retrospective I can only write now: six
months of running the Solid stack in real production, under real load. Where did the simplification
genuinely deliver? Where were the rough edges the launch glow had hidden? And does the “you don’t need
Redis” promise hold up once it’s carrying your actual traffic? This is the lessons-learned, written from
the other side of the enthusiasm.
What genuinely delivered
The good news first, because it’s the larger part: the core promise held. Running jobs and cache in Postgres, with no Redis in the stack, was not a compromise we spent six months regretting — it was the simplification it claimed to be, and the benefits were real and daily:
- One fewer system to operate, and you feel it constantly. No separate Redis to provision, monitor, secure, back up, and reason about during an incident. Every time something went wrong, the absence of a second datastore meant one fewer place to look and one fewer thing that could be the cause. The operational simplicity isn’t abstract; it shows up every single time you operate the system.
- Transactional enqueueing quietly eliminated a class of bug. Because jobs live in the same database, enqueuing a job in the same transaction as the data change that triggers it means they commit together or not at all. The old “the record saved but the job didn’t enqueue” (or vice versa) inconsistency simply stopped happening. We didn’t have to fix those bugs; they couldn’t occur.
- One backup, one set of tools, one mental model. Jobs and cache backed up with the database, inspectable with the SQL we already use. Debugging a stuck job meant querying a table, not learning a separate system’s introspection tools. The unified mental model is a real, sustained reduction in cognitive load.
For the great majority of what we asked of it, the Solid stack was exactly what was advertised, and six months in I’d reach for it again without hesitation. The “you don’t need Redis” claim is, for a normal production app, true.
The rough edges
Honesty requires the other side, because no stack is free of them, and the launch enthusiasm naturally underplayed these. The rough edges we actually hit:
- The database is doing more, and you have to watch it. Moving jobs and cache into Postgres means Postgres carries load it didn’t before — the job queue’s polling and churn, the cache’s reads and writes — all on the same database serving your application queries. For us it was well within capacity, but it’s load you must monitor, because a heavy job spike now shows up as load on your primary database, not on a separate Redis you could reason about in isolation. The simplification trades two systems you watch separately for one system you must watch more carefully.
- Cache performance is a different profile. Solid Cache, being database/disk-backed rather than in-memory, has different latency characteristics than Redis. For most of our caching it was completely fine — and the much larger capacity meant we could cache more. But for the hottest, most latency-sensitive cache paths, we did notice the difference, and had to be thoughtful about what we cached where. It’s not a drop-in performance equivalent to in-memory; it’s a different point on the curve, and you have to know which paths actually care.
- Table maintenance is real. Finished jobs and expired cache entries accumulate, and the cleanup needs to actually run. We had one incident early where job-table growth crept up because we hadn’t given the cleanup enough attention — a self-inflicted, easily-fixed problem, but a reminder that “jobs in the database” means database-table hygiene is now part of your operational routine.
None of these were dealbreakers, and all were manageable — but they’re the things the release-day write-up (including mine) didn’t dwell on, and they’re what you actually spend attention on once it’s in production.
The question everyone asks: did we miss Redis?
The honest answer: for this app, no — and that surprised even me a little. Going in, part of me expected to hit a wall where the database-backed approach couldn’t keep up and we’d have to reintroduce Redis. We didn’t. Our job volumes and cache needs, which are substantial but not extreme, sat comfortably within what Postgres handles, and the operational simplicity was worth more than the marginal performance Redis would have added. The cases where you genuinely still want Redis are the ones I flagged at the preview — extreme job throughput, a cache path that truly needs in-memory latency — and the honest finding after six months is that most apps, including ours, never actually hit those thresholds. The reflex to add Redis was, for us, exactly the over-engineering instinct the Solid stack was built to retire. We didn’t need it, and not having it made everything simpler.
What I’d tell someone starting now
Distilled advice from six months of production, for someone weighing the Solid stack on a new Rails 8 app:
- Default to it. For a normal app, the no-Redis Solid stack is the right starting point, and you very likely won’t outgrow it. Start simple; you can always add Redis later if a measured limit forces it.
- Monitor your database load from day one. Since jobs and cache now share it, watch the database’s load, and know the escape hatches exist (a separate queue database, a memory cache for a specific hot path) for if you ever need them.
- Set up the cleanup before you need it. Make job and cache cleanup part of your setup, not an afterthought, so table growth never becomes an incident.
- Don’t reintroduce Redis pre-emptively. Resist the habit of adding it “just in case.” Wait until you have a real, measured reason — and you may well never have one.
Verdict
Six months of running Rails 8’s Solid stack in production turned release-day enthusiasm into a settled, honest verdict: the core promise held. Jobs and cache in Postgres with no Redis delivered the operational simplification it claimed — one fewer system to operate (felt every single time you operate it), transactional enqueueing that quietly eliminated a class of consistency bug, and one backup, one toolset, one mental model. The rough edges are real but manageable: the database carries more load you must monitor, Solid Cache’s disk-backed profile differs from in-memory Redis on the hottest paths, and job/cache table maintenance is now part of your routine. The question everyone asks — did we miss Redis? — answered itself: for a normal app with substantial-but-not-extreme needs, no, and the threshold where you genuinely would was one we (like most apps) never hit, which is precisely the over-engineering reflex the Solid stack was built to retire. So the advice is to default to the Solid stack, monitor your database load from the start, set up cleanup early, and don’t reintroduce Redis until a measured limit actually forces it. The one-person-framework thesis, tested against real production rather than a launch demo, holds up — and that’s the strongest thing I can say about it.