N+1 queries silently destroy Rails application performance. While eager loading fixes individual cases, strict loading prevents them from happening in the first place. Rails 8 offers multiple strict loading modes that catch these issues during development, not after users complain about slow pages.
The Problem: Silent Performance Killers
Consider a typical blog application. Loading posts with their authors seems straightforward until the logs reveal dozens of queries firing where one should suffice. Traditional solutions involve sprinkling includes throughout the codebase, but this reactive approach misses queries until they cause problems.
Strict loading flips this model. Instead of hoping developers remember to eager load, Rails raises exceptions when lazy loading occurs. This surfaces N+1 queries immediately during development and testing.
Enabling Strict Loading Globally
The most aggressive approach enables strict loading for all models application-wide. This configuration ensures no lazy loading happens anywhere:
# config/environments/development.rb
Rails.application.configure do
# Raise errors when lazy loading associations
config.active_record.strict_loading_by_default = true
# Also catch violations in test environment
end
# config/environments/test.rb
Rails.application.configure do
config.active_record.strict_loading_by_default = true
endWith this configuration, any attempt to lazy load an association raises ActiveRecord::StrictLoadingViolationError. This aggressive stance works well for new applications or teams committed to eliminating all N+1 queries.
Model-Level Strict Loading
For existing applications, enabling strict loading per-model provides a gradual migration path. Start with models that cause the most performance issues:
# app/models/post.rb
class Post < ApplicationRecord
# Enable strict loading for all Post queries
self.strict_loading_by_default = true
belongs_to :author, class_name: 'User'
has_many :comments
has_many :taggings
has_many :tags, through: :taggings
has_one :featured_image, class_name: 'Image', as: :imageable
end
# app/models/user.rb
class User < ApplicationRecord
# Leave strict loading off while migrating
# self.strict_loading_by_default = true
has_many :posts
has_many :comments
endThis approach allows teams to tackle one model at a time, fixing eager loading issues before moving to the next model.
Query-Level Strict Loading
Sometimes strict loading should apply only to specific queries. The strict_loading method enables this granular control:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
# Strict loading only for this query
@posts = Post.strict_loading
.includes(:author, :tags)
.where(published: true)
.order(created_at: :desc)
.limit(20)
end
def show
# Load specific associations, strict load everything else
@post = Post.strict_loading
.includes(:author, :comments, :tags)
.find(params[:id])
end
def edit
# No strict loading needed for simple edit form
@post = Post.find(params[:id])
end
endThis pattern works well when certain views need more associations than others. The controller explicitly declares what gets loaded, and strict loading catches any missed associations.
Association-Level Configuration
Some associations should always be strict loaded, regardless of global settings. Configure this directly on the association:
# app/models/order.rb
class Order < ApplicationRecord
belongs_to :customer
# Always require explicit loading for line items
has_many :line_items, strict_loading: true
# Expensive association - force eager loading
has_many :shipments, strict_loading: true
has_many :tracking_events, through: :shipments, strict_loading: true
# Lightweight association - lazy loading acceptable
has_many :notes, strict_loading: false
end
# app/models/line_item.rb
class LineItem < ApplicationRecord
belongs_to :order
belongs_to :product, strict_loading: true
# Variant lookup can be expensive
belongs_to :variant, strict_loading: true
endThis configuration documents which associations are expensive and prevents accidental lazy loading even when global strict loading is disabled.
Handling Strict Loading Violations
When strict loading catches a violation, the exception message identifies exactly which association triggered it:
# Example error message:
# ActiveRecord::StrictLoadingViolationError:
# `Post` is marked for strict loading. The `comments` association
# cannot be lazily loaded.
# Fix by including the association:
@posts = Post.strict_loading.includes(:comments).all
# Or preload if includes causes issues:
@posts = Post.strict_loading.preload(:comments).allThe error message tells developers exactly what association needs eager loading, making fixes straightforward.
Strict Loading Modes
Rails 8 supports two strict loading modes that control behavior differently:
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
# :all - raises on any lazy load (default)
# :n_plus_one_only - only raises when iterating
self.strict_loading_mode = :n_plus_one_only
end
# config/environments/development.rb
Rails.application.configure do
config.active_record.strict_loading_by_default = true
# Only catch actual N+1 patterns, not single lazy loads
config.active_record.strict_loading_mode = :n_plus_one_only
endThe :n_plus_one_only mode provides a middle ground. Loading a single record's association won't raise an error, but iterating through a collection and lazy loading associations will. This catches the worst performance offenders while allowing occasional single-record lazy loads.
Testing Strict Loading Compliance
Automated tests can verify that controllers properly eager load associations:
# spec/requests/posts_spec.rb
RSpec.describe "Posts", type: :request do
describe "GET /posts" do
let!(:posts) { create_list(:post, 3, :with_author, :with_tags) }
it "eager loads associations without strict loading violations" do
# Enable strict loading for this test
Post.strict_loading_by_default = true
expect {
get posts_path
}.not_to raise_error
ensure
Post.strict_loading_by_default = false
end
end
end
# spec/support/strict_loading_helper.rb
module StrictLoadingHelper
def with_strict_loading(*models)
original_values = models.map { |m| [m, m.strict_loading_by_default] }.to_h
models.each { |m| m.strict_loading_by_default = true }
yield
ensure
original_values.each { |model, value| model.strict_loading_by_default = value }
end
end
RSpec.configure do |config|
config.include StrictLoadingHelper
endThis helper makes it easy to test strict loading compliance across the test suite without affecting other tests.
Production Considerations
Strict loading should generally stay disabled in production. A missed eager load causing an exception is worse than a slow query:
# config/environments/production.rb
Rails.application.configure do
# Never enable strict loading in production
config.active_record.strict_loading_by_default = false
endInstead, rely on development and test environments to catch violations, and use query logging or APM tools to identify any that slip through.
Summary
Strict loading transforms N+1 query prevention from a reactive debugging task into proactive development practice. Start with query-level strict loading on critical paths, expand to model-level configuration for problem areas, and consider application-wide enforcement for new projects. Combined with proper test coverage, strict loading ensures eager loading issues surface before reaching production.