Rails 8 in Production (2026): Performance, Security, and Hotwire Patterns That Scale
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 --updatein CI to catch known CVEs - Pin gem versions: Use exact versions for security-critical gems (bcrypt, jwt, devise)
- Review Brakeman: Run
brakeman --no-pagerbefore every deploy to catch static analysis warnings - Rotate secrets: Schedule quarterly rotation of
secret_key_baseand 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