This content originally appeared on DEV Community and was authored by Alex Aslam
Alright, Let’s set the scene. You’re code reviewing a Pull Request. The feature is solid, the tests are green. You scroll down to the controller and see this:
def index
@projects = Project.all
end
And in the view:
<% @projects.each do |project| %>
<h2><%= project.name %></h2>
<p>Lead by: <%= project.manager.name %></p> <!-- 🚨 Danger Zone -->
<% end %>
Your spidey-sense starts tingling. That innocent project.manager.name
is a siren’s call. It’s whispering, “I will generate a separate database query for every single project. I will bring your 95th percentile response time to its knees. I will do this silently.”
This is the N+1 query problem. It’s the most classic Rails performance anti-pattern. We’ve all been there. We’ve all fixed them with .includes(:manager)
. But why are we still finding them in code reviews? Why is our first line of defense a manual process or a performance test after the fact?
It’s time to change the game. It’s time to make N+1s fail fast—at development time, not in production. It’s time to talk about strict_loading
.
Beyond includes
: The Philosophical Shift
Using .includes
is treating the symptom. Enabling strict_loading
is curing the disease.
The old way is reactive: “Oh, look, an N+1 in the Skylight dashboard. Let’s go add an .includes
.”
The strict_loading
way is proactive: “My code tried to lazily load an association it shouldn’t have. It threw an exception on my local machine. I will fix it right now before this ever gets committed.”
This is a paradigm shift from performance optimization to correctness and intentionality. You are declaring, “This query must explicitly pre-load all the data it needs to function.” This is how senior engineers build robust, predictable systems.
What is strict_loading
?
Introduced in Rails 6.1, strict_loading
is a mode you can enable on any Active Record object or relation. When enabled, it forbids the lazy loading of associations. If you try to access an association that hasn’t been pre-loaded, it doesn’t make a sneaky query—it raises an ActiveRecord::StrictLoadingViolationError
.
This is your application’s compile-time check for N+1 queries.
How to Activate the Force Field
You can enable it in several ways, depending on the scope you want to cover.
1. On a single query:
# This specific query cannot lazy load.
@project = Project.strict_loading.find(params[:id])
2. On an entire relation:
# All projects from this relation cannot lazy load.
@projects = Project.strict_loading.all
3. On every instance of a model (in the model):
# The nuclear option. Every Project instance will be strict-loaded.
class Project < ApplicationRecord
strict_loading
end
4. Globally, for all models (in an initializer):
# config/initializers/strict_loading.rb
Rails.application.config.active_record.strict_loading_by_default = true
Senior Engineer Advice: Start with #1 and #2. The global option (#4) is incredibly powerful but can be incredibly disruptive in a large, established codebase. It’s the goal, but it’s a journey to get there.
The Real-World Guide: Adopting strict_loading
Without Losing Your Mind
Flipping the global switch on in a mature app will likely break everything. Here’s the strategic, phased approach.
Phase 1: Detection & Awareness
First, you need to know where the problems are. Enable strict loading in development and test environments, but make it “soft” initially.
# config/environments/development.rb
config.active_record.strict_loading = true # Turns it on
# config/environments/test.rb
config.active_record.strict_loading = true
But! To avoid getting blocked, handle the violations gracefully instead of raising exceptions. A great pattern is to log them aggressively.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
before_validation :log_strict_loading_violation
private
def log_strict_loading_violation
return unless strict_loading_violation? # This is a handy method Rails provides
# Log the hell out of this. Use a dedicated logger, notify Bugsnag, etc.
Rails.logger.warn(
"[STRICT_LOADING_VIOLATION] Tried to lazily load #{_reflect_on_association(strict_loading_violation_owner.name).klass} " \
"on #{self.class} ID #{id}. Call stack: #{caller.grep(/\/app\//).first(5).join("\n")}"
)
end
end
Now, your development log will be filled with warnings, not errors. This is your team’s N+1 discovery list. You can start fixing them one by one.
Phase 2: Targeted Strikes
Once you’ve identified hotspots, start enabling hard strict_loading
in new and refactored code.
Use it in new features by default:
class Api::V2::ProjectsController < Api::BaseController
def index
# This new, shiny API endpoint is N+1 proof from day one.
@projects = Project.strict_loading.includes(:manager, :tasks).all
render json: @projects
end
end
Use it when refactoring problematic controllers:
# Old, problematic action
def show
@project = Project.find(params[:id]) # Known for N+1s in the view
end
# Refactored, robust action
def show
@project = Project.strict_loading.includes(:manager, :milestones, :tasks).find(params[:id])
end
Phase 3: The Grand Finale (Global Enablement)
Once you’ve squashed the vast majority of violations from your logs and have built the muscle memory on the team, you can go for the global, hard enforcement.
# config/initializers/strict_loading.rb
Rails.application.config.active_record.strict_loading_by_default = true
You will still need escape hatches. There are legitimate edge cases.
The Judicious Escape Hatch:
Sometimes, you genuinely don’t know if you’ll need an association. For those rare cases, you can suppress strict loading.
# Maybe this is in a background job where performance is less critical,
# or in an admin panel where the query is unpredictable.
project = Project.find(params[:id])
project.strict_loading_off do
project.activities.each do |activity|
puts activity.user.name
end
end
Use this power sparingly. It should feel dirty.
Common Pitfalls & Advanced Patterns
The Serializer/View Layer Trap: This is the biggest one. You think you’ve pre-loaded everything in the controller, but then your JBuilder or ActiveModelSerializers view accesses
project.manager.current_department.name
. Your controller’sincludes(manager: :department)
just becameincludes(manager: { department: :current_department })
. This is frustrating but exactly the point—it forces you to be explicit about the data shape your view requires.-
Scopes and Conditional Pre-loading: What if you only want to pre-load an association conditionally?
# Bad: Will break in strict mode if `project.manager` is accessed @projects = Project.all @projects = @projects.includes(:manager) if params[:include_manager] # Good: Use a pattern that always pre-loads, but perhaps with a conditional @projects = Project.strict_loading.all @projects = @projects.includes(:manager) # Just always include it. The query overhead is often negligible. # Better (Advanced): Use `preload` or `eager_load` with a conditional scope that doesn't break strict loading. # This is complex but possible.
It Doesn’t Replace
includes
: You still have to know what to pre-load.strict_loading
doesn’t write the.includes
for you. It just forces you to do it correctly. Tools likebullet
are still useful companions in the detection phase to suggest what you might have missed.
Conclusion: From Performance Hack to Engineering Discipline
Adopting strict_loading
is more than a technical change; it’s a cultural one. It moves the responsibility for query performance from the “ops” team or the “performance review” stage and bakes it directly into the development process.
It makes N+1 queries a development-time failure, a concept that should make every senior engineer’s heart sing. It encourages thoughtful API design, where the data requirements of a response are explicit and intentional.
Start small, log aggressively, and work towards making your entire application N+1-proof by default. It’s one of the highest-leverage investments you can make in your application’s long-term performance and health.
This content originally appeared on DEV Community and was authored by Alex Aslam