rails ruby web-development

Rails 8 Production Features: Real Costs and Failure Modes

- 11 min read

What Rails 8's Solid stack actually costs at scale, the failure modes you hit in production, and the upgrade order that minimizes downtime risk.

Rails 8 production features including Solid Queue, Solid Cache, Kamal deployment, and built-in authentication

Most Rails 8 coverage explains what each feature does. This post covers what they cost in production, where the Solid stack breaks under load, and the upgrade order that minimizes the chance of a bad week.

If you want a deep dive on any single feature, the dedicated posts at the bottom go into configuration, benchmarks, and gotchas. This post is the integration view: how the pieces interact, where they fail together, and how they reshape your infrastructure budget at different scales.

What’s Actually Different in Rails 8

Rails 8’s headline is dropping Redis as a default dependency by replacing it with database-backed alternatives for jobs, caching, and WebSockets. The rest is incremental: Kamal ships in the box, authentication is generated, Propshaft replaces Sprockets, and async queries get a meaningful boost.

Component Rails 7 Typical Rails 8 Default Production-Ready Status
Background Jobs Sidekiq + Redis Solid Queue Adopt below 100K jobs/hour
Caching Redis or Memcached Solid Cache Adopt below 1GB hot cache
WebSockets Redis (Action Cable) Solid Cable Adopt for moderate fan-out
Deployment Capistrano or Heroku Kamal Adopt - mature in v2
Asset Pipeline Sprockets Propshaft Skip if on esbuild or Vite
Authentication Devise Built-in generator Adopt for MVPs only
Frontend Webpack/esbuild Import maps Skip if your build step works
Async Queries Manual threading .load_async Adopt - low risk

The Status column matters more than the feature list. Adopting everything Rails 8 ships isn’t the goal. Picking the parts that fit your team’s existing stack is.

The Real Cost: When the Solid Stack Saves Money

Dropping Redis saves about $15-30/month on managed services like Upstash or AWS ElastiCache at small-app scale. That number is real but misleading. Here’s what actually happens to your infrastructure bill as load grows.

Small Apps (Under 10K Daily Active Users)

Net savings: $15-30/month. Postgres handles the additional load comfortably on a $25-50/month instance. This is the scenario the Rails 8 marketing is built around, and it works. Single VPS, single Postgres, the whole Solid stack on top.

Medium Apps (10K to 100K DAU)

Net savings: roughly break-even. The Solid stack pushes meaningful load onto Postgres. Cache writes, job polling, and Action Cable subscriptions all hit the same database. Most teams need to size Postgres up by one tier ($50-100/month delta), which roughly cancels the Redis savings.

Large Apps (100K+ DAU)

Net cost: $50-200/month more than the Redis-based equivalent. At this scale you either run a dedicated Postgres for the Solid stack (paying for a second database) or your primary Postgres needs more headroom than Redis would have required. The Solid stack is still operationally simpler, just not cheaper.

The cost story flips because Redis is purpose-built for cache and queue workloads at high write volume. Postgres is more general-purpose and pays for that flexibility with higher per-operation cost when pushed hard. None of this is a reason to avoid the Solid stack. Just don’t expect linear scaling on the savings story.

Failure Modes: The Solid Stack on a Single Database

Running Solid Queue, Solid Cache, and Solid Cable against your primary Postgres is the default Rails 8 configuration. It’s also where most teams hit their first production incident. Here’s what fails first, in order of how often you’ll see it.

Connection Pool Exhaustion

Each Solid component holds connections. Solid Queue’s worker pool, Solid Cache’s writes, and Solid Cable’s subscription handlers all share your Rails connection pool by default. Under load, they compete with web request handlers for the same connections.

# config/database.yml - the wrong way (everything shares one pool)
production:
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

# config/database.yml - the right way for the Solid stack
production:
  primary:
    pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  queue:
    database: app_production_queue
    pool: 10
    migrations_paths: db/queue_migrate
  cache:
    database: app_production_cache
    pool: 5
    migrations_paths: db/cache_migrate

Splitting databases (or at minimum splitting connection pools) per Solid component prevents a job spike from starving web requests. Rails 8’s multi-database support handles this cleanly once you opt in.

Autovacuum Lag on Hot Tables

Solid Cache writes are heavy. Every cache miss writes a row, every TTL expiration deletes one. On Postgres, this churn fragments indexes and triggers frequent autovacuum runs. If your solid_cache_entries table grows past a few million rows on a small instance, autovacuum can fall behind, and query times creep up across the entire database.

The fix is either tuning autovacuum more aggressively on cache tables specifically, or running Solid Cache against a dedicated Postgres instance where its churn doesn’t compete with your application data.

Long-Running Cable Connections

Solid Cable holds an open connection per WebSocket subscriber. On Postgres with default connection limits (around 100), you can hit the cap with surprisingly few simultaneous users if your app uses Action Cable broadly. PgBouncer in transaction mode breaks Cable’s persistent subscriptions, so you can’t pool your way out. Either size your Postgres connection limit up, or keep Action Cable on Redis for now.

When the Failure Modes Bite

For an internal B2B dashboard or any data-heavy admin tool, the failure cascade usually goes:

  1. Sustained traffic crosses ~50K cache writes/hour and autovacuum starts running constantly
  2. A background job spike (CSV export, batch email, nightly reconciliation) competes for the same connection pool
  3. Action Cable subscriptions during peak hours saturate the connection limit
  4. Web requests queue, p99 latency climbs from 200ms to 2s+, alerts fire

This is recoverable. All three Solid components support being moved to a separate database with no application code changes. But the default “everything on primary Postgres” configuration is a small-app pattern, not a forever-pattern.

Upgrade Order: What Breaks If You Do It Wrong

Most upgrade pain comes from doing things in the wrong order, not from any individual change. Here’s the order that minimizes blast radius.

Step Change Why This Order Rollback Difficulty
1 Rails 7.x to 8.0 core Get framework upgrade stable before changing components Easy - bundle version pin
2 Sprockets to Propshaft (optional) Asset pipeline change is isolated; do before adding Solid stack Medium - manifest format differs
3 Add Solid Cache Lowest-risk Solid component; failures degrade silently Easy - swap cache_store config
4 Add Solid Queue alongside Sidekiq Run both for a release, route new jobs to Solid Queue, monitor Easy - keep Sidekiq config
5 Cut over to Solid Queue Move existing job classes after a soak period Medium - drain Sidekiq queue first
6 Add Solid Cable (optional) Only if you actually use Action Cable; do last because failures are loud Hard - WebSocket reconnect storms

The Most Common Mistake

Switching Sidekiq to Solid Queue first, before validating that Postgres can handle the load. Solid Queue failures cascade: if jobs back up, Postgres write load climbs, web requests slow down, more jobs queue, and you can’t deploy the rollback because the workers can’t drain. Always run Solid Queue alongside Sidekiq for at least one release before cutting over.

Async Queries: The Underrated Win

.load_async runs ActiveRecord queries in parallel against the connection pool instead of sequentially. On dashboard pages with multiple unrelated queries, this routinely cuts page load by 30-50%.

def dashboard
  @transactions = Transaction.recent.load_async
  @metrics = Metric.calculate_monthly.load_async
  @users = User.active.load_async

  # Each query starts immediately on a separate connection;
  # results are awaited when first accessed in the view
end

The catch: each load_async call uses a connection from the pool. On a 5-connection pool, three async queries plus the main thread can saturate the pool. For dashboards with 5+ parallel queries, you’ll need to size your pool up or you’ll see connection-checkout latency cancel out the parallelism gains.

This is one of the few Rails 8 features that’s pure upside on the typical app. It works, it’s safe to enable per-query, and it’s invisible to users beyond making pages faster. For more on what to optimize first, see database optimization techniques for Rails.

Hotwire: Turbo Frame Auto-Refresh

The most genuinely useful Hotwire change in Rails 8 is the refresh: attribute on Turbo Frames. It enables polled refresh without writing any JavaScript or setting up Action Cable.

<%= turbo_frame_tag "metrics",
                     src: metrics_path,
                     refresh: "every 30s" do %>
  <%= render "loading" %>
<% end %>

For internal dashboards, admin views, and any near-real-time use case, this replaces what used to require a Stimulus controller plus a setInterval plus careful connection management. It’s also gentler on infrastructure than WebSockets for low-frequency updates.

The tradeoff is that polling is stateless. The server can’t push when something changes. For an inventory dashboard updating every 30 seconds, that’s fine. For a chat app, you still want Action Cable.

Authentication Generator: Useful but Limited

rails generate authentication produces a complete password-based auth system with sessions, password reset, and bcrypt hashing. It’s enough for an MVP, an admin panel, or an internal tool.

What it doesn’t ship: OAuth, account confirmation emails, account lockout after failed logins, two-factor authentication, sudo mode for sensitive actions, or session revocation across devices. If you need any of those, Devise or rodauth-rails saves you from building them yourself.

Decision rule: use the generator if you’d otherwise build auth from scratch. Use Devise if you’d otherwise pull it in within three months anyway.

Features to Skip on Day One

Not every Rails 8 default is an upgrade for every team.

Propshaft is a clean replacement for Sprockets, but if you’re already running esbuild, Vite, or jsbundling-rails, the migration is a sideways move that costs you a sprint. Stay on what works.

Import maps make sense for apps with no build step. If you have any TypeScript, JSX, or non-trivial CSS preprocessing, you need a bundler anyway, and import maps add a second mental model on top of the one you already have.

Solid Cable is the Solid component most likely to bite you. WebSocket connection counts hit Postgres connection limits faster than most teams expect. If your existing Redis-backed Action Cable works, leave it alone for now. The operational savings don’t outweigh the migration risk.

When to Bring Redis Back

Concrete thresholds where Redis is the right call:

  • Sustained job throughput above 100,000/hour
  • Cache hit paths that depend on sub-millisecond reads (real-time bidding, ad serving, leaderboards)
  • Action Cable connection counts above 5,000 simultaneous subscribers
  • Need for advanced job features: Sidekiq Pro batches, rate limiting, encrypted job arguments
  • Cache size requirements above 5-10GB where Postgres storage cost exceeds Redis

Below these thresholds, the Solid stack saves operational complexity. Above them, Redis pays for itself by keeping load off your primary database.

When NOT to Upgrade Yet

Stay on Rails 7 for now if any of these apply:

  • You’re on Sprockets with custom asset processing that hasn’t been ported to Propshaft
  • Your gem dependency chain hasn’t fully updated to Rails 8 (check bundle outdated --filter-strict)
  • You’re mid-migration on something else - don’t run two upgrades concurrently
  • You operate at a scale where Sidekiq Pro/Enterprise features are load-bearing and you don’t have time to evaluate alternatives

Rails 8 is stable, but stable doesn’t mean free to upgrade right now. Most apps get more value waiting until they have a 1-2 week window where an unplanned rollback wouldn’t be catastrophic.

The Bottom Line

Rails 8’s Solid stack is real infrastructure consolidation for small and medium apps. The savings story is true at small scale and break-even at medium scale. Past that, you’re choosing operational simplicity over raw performance, which is a fair trade but not the marketing pitch.

Adopt unconditionally: async queries, Kamal, the auth generator (for MVPs), and Hotwire frame auto-refresh. Adopt conditionally on your existing stack: Propshaft, import maps, and Solid Cable. Adopt with capacity planning: Solid Queue and Solid Cache.

Most importantly, the upgrade order matters more than any individual feature. Rails core first, asset pipeline second, Solid components last and one at a time. Treat any Solid component switch as a capacity planning event, not just a config change.


Need help upgrading to Rails 8? I help teams with Rails upgrades, Solid stack capacity planning, and architecture decisions. If you’re planning a migration and want a second opinion on the order of operations, reach out at nikita.sinenko@gmail.com.

Further Reading