RubyLearning

Helping Ruby Programmers become Awesome!

Rails 8 in Production (2026): Performance, Security, and Hotwire Patterns That Scale

April 12, 2026 | By RubyLearning

Rails 8 has been running in production long enough that patterns have emerged. Teams know which defaults hold up, which need tuning, and where Hotwire shines versus where it creates friction. This guide collects the practical lessons — performance checklists, security hardening, and Hotwire patterns — that intermediate Rails developers need to ship confidently in 2026.

We are not covering initial setup or basic CRUD. This is for teams already running Rails 8 (or upgrading soon) who want a production-ready reference they can work through in an afternoon.

Performance Checklist

Rails 8 gives you better defaults out of the box — Solid Cache, Solid Queue, query log tags — but defaults alone do not make a fast application. Walk through each layer.

Database

The biggest performance wins still come from the database layer. Rails 8 makes query analysis easier with automatic query log tags in development, but you need to act on what they reveal.

# config/environments/production.rb

# Enable strict loading globally to catch N+1 queries
config.active_record.strict_loading_by_default = true

# Log slow queries (PostgreSQL example)
config.active_record.warn_on_records_fetched_greater_than = 500

# Use connection pooling sized to your Puma thread count
config.active_record.pool = ENV.fetch("RAILS_MAX_THREADS") { 5 }
# Fix N+1 queries before they reach production
class OrdersController < ApplicationController
  def index
    # BAD: N+1 on customer and line_items
    # @orders = Order.recent

    # GOOD: eager load known associations
    @orders = Order.includes(:customer, :line_items)
                   .where(created_at: 30.days.ago..)
                   .order(created_at: :desc)
                   .limit(50)
  end
end

Caching

Rails 8 defaults to Solid Cache (database-backed) for new apps. It eliminates the Redis dependency for caching, but it is not zero-configuration for production workloads.

# config/environments/production.rb
config.cache_store = :solid_cache_store

# Set max cache size to prevent unbounded DB growth
# config/solid_cache.yml
production:
  max_size: <%= 256.megabytes %>
  max_age: <%= 1.week.to_i %>
# Use Russian doll caching in views
# app/views/orders/_order.html.erb
<% cache order do %>
  <div class="order-card">
    <h3><%= order.number %></h3>
    <% cache order.customer do %>
      <p><%= order.customer.name %></p>
    <% end %>
  </div>
<% end %>

Background Jobs

Solid Queue replaces the need for Redis-backed job processors in many workloads. For high-throughput scenarios, measure before committing — Sidekiq still outperforms Solid Queue on raw enqueue/dequeue speed at scale.

# config/solid_queue.yml
production:
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: ["critical", "default", "low"]
      threads: 5
      processes: 2

# Use queue priorities to separate latency-sensitive work
class SendWelcomeEmailJob < ApplicationJob
  queue_as :critical

  def perform(user)
    UserMailer.welcome(user).deliver_now
  end
end

class GenerateReportJob < ApplicationJob
  queue_as :low

  def perform(report_id)
    Report.find(report_id).generate!
  end
end

Profiling

Do not guess. Profile first, then optimize. Rails 8's query log tags combined with standard profiling tools give you actionable data.

# Gemfile — profiling tools for development/staging
group :development do
  gem "rack-mini-profiler"
  gem "stackprof"
  gem "memory_profiler"
end

# Quick memory check for a specific action
# rails runner script
MemoryProfiler.report {
  OrdersController.new.index
}.pretty_print(to_file: "tmp/orders_memory.txt")

Security Hardening Checklist

Rails has strong security defaults, but production hardening goes beyond the framework. Work through each item — many are one-time configurations that pay off permanently.

Sessions and Cookies

# config/initializers/session_store.rb
# Rails 8 supports database-backed sessions out of the box
Rails.application.config.session_store :active_record_store,
  key: "_myapp_session",
  secure: Rails.env.production?,
  httponly: true,
  same_site: :lax,
  expire_after: 12.hours

CSRF and Content Security Policy

# config/initializers/content_security_policy.rb
Rails.application.configure do
  config.content_security_policy do |policy|
    policy.default_src :self
    policy.font_src    :self, "https://fonts.gstatic.com"
    policy.img_src     :self, :data, "https:"
    policy.object_src  :none
    policy.script_src  :self
    policy.style_src   :self, "https://fonts.googleapis.com"
    policy.connect_src :self
    policy.frame_ancestors :none
  end

  # Generate nonces for inline scripts (required for Turbo)
  config.content_security_policy_nonce_generator =
    ->(request) { request.session.id.to_s }
  config.content_security_policy_nonce_directives = %w[script-src]
end

Authentication Best Practices

Rails 8's built-in authentication generator gives you a solid starting point. Layer these hardening steps on top.

# Rate limit login attempts (Rails 8 native)
class SessionsController < ApplicationController
  rate_limit to: 10, within: 3.minutes, only: :create,
    with: -> { redirect_to new_session_url, alert: "Too many attempts." }

  def create
    user = User.authenticate_by(
      email: params[:email],
      password: params[:password]
    )

    if user
      start_new_session_for(user)
      redirect_to root_path
    else
      # Generic message — never reveal which field was wrong
      flash.now[:alert] = "Invalid email or password."
      render :new, status: :unprocessable_entity
    end
  end
end

Secrets and Dependency Hygiene

  • Credentials: Use rails credentials:edit --environment production — never store secrets in ENV vars that get logged
  • Audit gems weekly: Run bundle-audit check --update in CI to catch known CVEs
  • Pin gem versions: Use exact versions for security-critical gems (bcrypt, jwt, devise)
  • Review Brakeman: Run brakeman --no-pager before every deploy to catch static analysis warnings
  • Rotate secrets: Schedule quarterly rotation of secret_key_base and API tokens

Tracking security advisories manually does not scale. Automated monitoring tools like IntelDaily can alert you when new Rails CVEs are discussed online, giving your team a head start on patching before the next advisory drops.

Hotwire Patterns That Scale

Hotwire (Turbo + Stimulus) is Rails 8's default frontend stack. The patterns below have proven reliable in production applications with thousands of concurrent users.

Pattern: Turbo Frame for Inline Editing

<!-- app/views/comments/_comment.html.erb -->
<%= turbo_frame_tag comment do %>
  <div class="comment">
    <p><%= comment.body %></p>
    <%= link_to "Edit", edit_comment_path(comment) %>
  </div>
<% end %>

<!-- app/views/comments/edit.html.erb -->
<%= turbo_frame_tag @comment do %>
  <%= form_with model: @comment do |f| %>
    <%= f.text_area :body %>
    <%= f.submit "Save" %>
    <%= link_to "Cancel", @comment %>
  <% end %>
<% end %>

Pattern: Turbo Stream for Live Updates

# app/controllers/comments_controller.rb
def create
  @comment = @post.comments.build(comment_params)

  if @comment.save
    respond_to do |format|
      format.turbo_stream  # renders create.turbo_stream.erb
      format.html { redirect_to @post }
    end
  else
    render :new, status: :unprocessable_entity
  end
end

# app/views/comments/create.turbo_stream.erb
<%= turbo_stream.prepend "comments", @comment %>
<%= turbo_stream.update "comment_form" do %>
  <%= render "comments/form", comment: Comment.new %>
<% end %>
<%= turbo_stream.update "comment_count", @post.comments.size %>

Pattern: Stimulus for Client-Side Behavior

// app/javascript/controllers/auto_submit_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 300 } }

  search() {
    clearTimeout(this.timeout)
    this.timeout = setTimeout(() => {
      this.formTarget.requestSubmit()
    }, this.delayValue)
  }
}

// Usage in view:
// <div data-controller="auto-submit">
//   <%= form_with url: search_path, method: :get,
//         data: { auto_submit_target: "form" } do |f| %>
//     <%= f.search_field :q, data: { action: "input->auto-submit#search" } %>
//   <% end %>
// </div>

Hotwire Anti-Patterns to Avoid

Anti-Pattern Why It Hurts Do This Instead
Nesting Turbo Frames deeply Requests cascade; one failure breaks the chain Keep frames at one level; use Turbo Streams for multi-region updates
Using Turbo Streams for entire pages Loses browser history, breaks back button Use Turbo Drive for full navigations; Streams for partials only
Fat Stimulus controllers Business logic in JS that should live server-side Keep Stimulus thin — dispatch events, toggle classes, submit forms
Broadcasting every model change Database load and WebSocket noise at scale Broadcast selectively; debounce with after_commit

The common thread: let the server do the heavy lifting. Hotwire works best when Stimulus controllers are small and Turbo handles the HTML transport. If you find yourself writing complex client-side state management, you have drifted from the Hotwire mental model. For teams evaluating how AI coding assistants handle Hotwire scaffolding, the anti-pattern table above is a useful benchmark for generated code quality.

Migration Notes for Teams Upgrading to Rails 8

If your team is coming from Rails 6.1 or 7.x, these are the changes that affect daily development most. See our full upgrade guide for the step-by-step process.

Area Old Way Rails 8 Way
Asset pipeline Sprockets + Webpacker Propshaft + import maps (or esbuild/Vite)
Background jobs Redis + Sidekiq Solid Queue (DB-backed) or keep Sidekiq
Caching Redis/Memcached Solid Cache (DB-backed) or keep Redis
WebSockets Action Cable + Redis adapter Solid Cable (DB-backed) or keep Redis
Authentication Devise / custom Built-in generator + has_secure_password
Deployment Capistrano / Heroku Kamal 2 (or keep existing tooling)
Enum syntax enum status: {} enum :status, {}

Key point: None of these changes are forced. The Solid stack is the default for new apps. Existing apps keep their infrastructure and adopt new defaults incrementally. Set config.load_defaults 8.0 only after you have reviewed what each default changes.

# Review exactly what load_defaults changes before enabling
# Run in rails console:
Rails.application.config.instance_variables.sort.each do |var|
  puts "#{var}: #{Rails.application.config.instance_variable_get(var)}"
end

# Adopt defaults one at a time via config/initializers/new_defaults.rb
# instead of flipping load_defaults all at once
Rails.application.config.active_record.belongs_to_required_by_default = true
Rails.application.config.action_controller.raise_on_missing_callback_actions = true

FAQ

Should I replace Redis with Solid Cache and Solid Queue in an existing app?

Only if Redis is a pain point for your team (operational overhead, cost, or complexity). If Redis is working well, keep it. The Solid stack is designed to eliminate infrastructure dependencies for teams that do not need Redis's throughput. Benchmark your specific workload before migrating.

How do I handle CSRF tokens with Turbo Stream responses?

Turbo handles CSRF automatically for form submissions via the csrf-token meta tag. If you are making custom fetch requests, include the token from document.querySelector('meta[name="csrf-token"]').content in your request headers. Turbo Stream broadcasts over WebSockets bypass CSRF because they are server-initiated.

Is Hotwire enough for complex interactive UIs, or do I still need React?

Hotwire handles most CRUD-heavy, form-driven interfaces well. For highly interactive features like real-time collaborative editing, drag-and-drop builders, or rich data visualizations, a JavaScript framework may still be the better choice. Many teams use Hotwire for 90% of pages and embed React or Vue components for the remaining 10%.

What is the recommended way to profile a slow Rails 8 endpoint?

Start with rack-mini-profiler to identify whether the bottleneck is in the database, view rendering, or Ruby code. Use stackprof for CPU profiling and memory_profiler for allocation analysis. In production, use Rails 8's query log tags to trace slow SQL back to the originating controller action.

How do I set up Content Security Policy without breaking Turbo?

Use nonce-based CSP for script tags. Rails 8 supports nonce generation via content_security_policy_nonce_generator in your CSP initializer. Add script-src to the nonce directives list. Turbo's JavaScript is loaded via script tags that automatically receive the nonce when you use javascript_include_tag with nonce: true.

Can I use Rails 8's built-in authentication alongside Devise?

Not recommended in the same app. They use different session management approaches that will conflict. Choose one: use the built-in generator for simpler apps, or keep Devise for apps that need OAuth, two-factor auth, or its ecosystem of extensions. If you are starting fresh, the built-in generator is the simpler path.

Production Rails is not about using every new feature — it is about choosing the right defaults for your workload, hardening the security surface, and keeping your frontend patterns simple enough that the next developer can understand them without a guide.

Tags: Rails 8 Performance Security Hotwire Turbo Production 2026