Rails strict loading: a Ruby on Rails guide for developers
N+1 query problems are one of the most common performance killers in Rails applications, yet they often slip through to production because they're invisible during local development. Rails strict loading: a Ruby on Rails guide for developers addresses this by catching lazy loading violations before they reach your users. This guide shows you how to enable strict loading across your models and relations, understand what triggers it, and fix the underlying query problems efficiently.
Understanding N+1 queries and why they matter
Before diving into strict loading, let's establish why N+1 queries are problematic. Imagine you're loading a list of users and need their associated posts:
users = User.all
users.each do |user|
puts user.posts.count
endThis executes one query to fetch all users, then one additional query per user to fetch their posts—if you have 1,000 users, that's 1,001 queries. The performance impact grows exponentially with dataset size, and it's especially painful in real-world e-commerce or SaaS applications where product catalogs and customer lists are large.
Rails strict loading prevents this by raising an exception the moment your code tries to access an unloaded association, forcing you to fix the problem at development time rather than discovering it in production metrics.
Enabling strict_loading on relations
The simplest way to enforce strict loading is to scope individual queries. When you chain strict_loading onto a relation, Rails will raise an ActiveRecord::StrictLoadingViolationError if any code attempts to load an association that wasn't explicitly included:
users = User.strict_loading.all
users.each do |user|
puts user.posts.count # Raises ActiveRecord::StrictLoadingViolationError
endThis approach is useful for specific queries where you know N+1 is a risk. It gives you fine-grained control without affecting the entire application. You might use this pattern in performance-critical endpoints, like an admin dashboard where you're loading large datasets:
class Admin::UsersController < ApplicationController
def index
@users = User.strict_loading.includes(:profile, :organization).page(params[:page])
end
endThe strict_loading call pairs well with includes, which we'll cover later. In this scenario, if a view or helper accidentally tries to access an unloaded association on @users, the error surfaces immediately rather than silently adding queries.
Enabling strict_loading on models globally
For broader protection, you can enable strict loading across an entire model class. This is the real power move for catching lazy loading throughout your codebase:
class User < ApplicationRecord
has_many :posts
has_many :comments
strict_loading_by_default
endWith strict_loading_by_default, every instance of User loaded from the database will enforce strict loading. Any attempt to access user.posts without explicitly preloading it will raise an error:
user = User.find(1)
user.posts # Raises ActiveRecord::StrictLoadingViolationErrorYou can override this for specific queries using strict_loading(false):
user = User.strict_loading(false).find(1)
user.posts # Works fine, but do this sparinglyIn production systems we've worked on—CRM platforms, e-commerce backends, and SaaS dashboards—enabling this model-level setting has been transformative. It forces the entire team to think about query performance from the start, rather than retro-fitting eager loading months later when performance becomes a crisis.
Raises on N+1 queries: how strict loading catches problems
When strict loading is enabled and your code tries to lazy load an association, Rails doesn't silently execute another query. Instead, it raises ActiveRecord::StrictLoadingViolationError with a clear message:
ActiveRecord::StrictLoadingViolationError:
Post was not eagerly loaded with UserThis error tells you exactly what went wrong: you tried to access posts on a User without preloading it. The stack trace then points you to the line in your code causing the problem.
Let's walk through a real scenario. In a ride-hailing backend, you might load drivers and need their vehicle information:
class Driver < ApplicationRecord
has_one :vehicle
strict_loading_by_default
end
class DriverSerializer
def initialize(driver)
@driver = driver
end
def as_json
{
id: @driver.id,
name: @driver.name,
vehicle_type: @driver.vehicle.vehicle_type # ERROR if vehicle not included
}
end
end
# In controller
drivers = Driver.all # This will now raise on serialization
drivers.map { |d| DriverSerializer.new(d).as_json }Without strict loading, this code silently fires N queries. With it enabled, you immediately see the problem and can fix it at the source by including the association.
Enable in development and tests
While you could enable strict loading everywhere, the pragmatic approach is to activate it in development and test environments first. This catches issues early without affecting production where legacy code might still have lazy loading patterns:
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
if Rails.env.development? || Rails.env.test?
strict_loading_by_default
end
endThis configuration gives your team time to identify and fix N+1 problems in a controlled setting. Your test suite becomes a guardian: if any test triggers lazy loading, it fails immediately, and you fix it before merging.
In a mature test suite—which we recommend for any production Rails application—this approach is powerful. Your CI pipeline will catch N+1 violations during automated testing, long before code reaches review or staging.
You can also enable strict loading more selectively in development for specific features or controllers:
class PostsController < ApplicationController
before_action :enable_strict_loading, only: [:index, :show]
def index
@posts = Post.includes(:author, :comments).all
end
private
def enable_strict_loading
Post.strict_loading(true)
end
endThis hybrid approach lets you introduce strict loading incrementally, testing it on high-risk endpoints first.
Fix with includes: the solution pattern
Once strict loading raises an error, the fix is almost always to use includes to preload the association. includes tells Rails to fetch related data in a separate query, avoiding lazy loading:
users = User.strict_loading.includes(:posts).all
users.each do |user|
puts user.posts.count # Works; posts already loaded
endRails is smart about includes. It automatically chooses between a JOIN query and separate queries based on the association type and complexity. For a has_many relationship, it typically runs two queries:
# Query 1: fetch all users
SELECT * FROM users
# Query 2: fetch all posts for those users
SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)This is N+1's elegant solution. Instead of one query per user, you get one query per relation type.
For nested associations, use dot notation or hash syntax:
users = User.strict_loading.includes(:posts, :profile).all
# or
users = User.strict_loading.includes(posts: :comments, profile: :avatar).allIn a travel booking platform we worked on, including nested associations on searches was critical. Users expected fast search results, and strict loading made sure our queries stayed efficient as features grew.
Sometimes includes` isn't enough. For very complex queries or when you need only a subset of records, use joins or raw SQL, but mark the association as loaded so strict loading doesn't complain:
users = User.joins(:posts)
.select('users.*, COUNT(posts.id) as post_count')
.group('users.id')
.strict_loading(false)
# Or more explicitly:
users = User.includes(:posts).all
users.each { |u| u.association(:posts).loaded! }Common mistakes
Mistake 1: Disabling strict loading everywhere to avoid errors. Some teams enable strict loading, encounter errors in tests, and disable it globally with strict_loading_by_default: false. This defeats the purpose. Instead, fix the underlying queries. The effort is worth it.
Mistake 2: Using strict loading only in production. Strict loading is most valuable in development and tests, where you can fix issues cheaply. Enabling it only in production means errors reach your users or production logs, which is too late.
Mistake 3: Including associations you don't use. If a query includes an association but the code doesn't access it, you're fetching unnecessary data. Review your includes periodically and remove unused ones. Tools like unused_includes (gem or custom analysis) help identify these.
Mistake 4: Forgetting strict loading in background jobs or async code. Lazy loading violations often hide in Sidekiq workers, ActionMailer, or ActiveJob. Make sure strict loading covers these contexts too, or you'll find N+1 problems only after they cause production incidents.
Key takeaways
- Strict loading catches lazy loading violations at development time, preventing N+1 queries from reaching production.
- Enable
strict_loading_by_defaulton models or usestrict_loadingon individual queries for fine-grained control. - Activate strict loading in development and test environments first; the early feedback is invaluable for building fast applications.
- Fix violations with
includes, which preloads associations and transforms N+1 queries into efficient batch queries. - Don't disable strict loading to silence errors; instead, fix the underlying queries. The initial effort saves significant debugging time later.
- Monitor background jobs, serializers, and async tasks—lazy loading violations hide in these places and are often missed during code review.






Comments
Loading comments…