Turn Rails N+1s Into Hard Errors With Active Record strict_loading
Active Record strict_loading turns lazy-loaded associations into explicit errors, making N+1 queries fail fast in development and test before they reach production.
17 Aug 2025, 15:54 UTC

N+1 queries are silent in Rails. You write posts.each { |p| p.author.name } and Active Record happily issues one query for the posts and then one more for each author. In development it feels fine. In production it shows up as latency spikes you have to hunt with query logs.
Active Record strict_loading, supported since Rails 6.1, changes the contract: a lazy-loaded association is an error, not a hidden query. The useful takeaway is to make the bug fail fast in development and test, then fix it with explicit eager loading before it reaches production.
What strict_loading actually changes
Lazy loading means an association is only queried when you first access it. Eager loading with includes, preload or eager_load fetches the association up front in one or a few queries.
strict_loading flips the default behavior for a relation or model. When it is active, accessing an unloaded association raises ActiveRecord::StrictLoadingViolationError instead of issuing a query. The error points to the exact call site that triggered the lazy load, which is more actionable than counting queries after the fact.
strict_loading does not change how associations work, it only guards them. It does not catch repeated pluck loops, manual counter cache queries, or other inefficient patterns.
Where to turn it on without breaking production
Three scopes exist, from narrow to broad.
Per query, for a single spot you are investigating:
Post.all.strict_loading.each { |p| p.author.name }Per model, to enforce a class by default:
class Post < ApplicationRecord
self.strict_loading_by_default = true
belongs_to :author
endGlobally, via config. In config/environments/development.rb or test.rb:
config.active_record.strict_loading_by_default = trueRun the change in a Rails console as a developer with app read access. The risk is high for a mature app: enabling globally will raise on existing endpoints that rely on lazy loading. A pragmatic rollout is development and test first, let the test suite and local browsing surface violations, then remediate before considering production.
Some Rails versions expose a log-only mode for gradual adoption. Option names and availability are version-sensitive, so check your Rails version's Active Record configuration guide before relying on :log behavior.
Worked example with Posts and authors
Assume Post belongs_to :author and Author has_many :posts.
Without strict_loading, this is a classic N+1:
posts = Post.limit(10)
posts.each { |p| puts p.author.name }With strict_loading on the relation:
posts = Post.strict_loading.limit(10)
posts.each { |p| p.author }Expected behavior is an ActiveRecord::StrictLoadingViolationError raised on the first access to p.author, indicating the association was not preloaded.
The fix is explicit eager loading:
posts = Post.includes(:author).strict_loading.limit(10)
posts.each { |p| puts p.author.name }With includes the association is preloaded, strict_loading stays satisfied, and the log shows a batched load for authors rather than one per post. You can verify the result by running the two variants in rails console and observing whether the violation is raised and whether the SQL log shows a single authors query after the includes.
You can also scope enforcement to a specific association:
class Post < ApplicationRecord
belongs_to :author, strict_loading: true
endThis raises only when author is lazy loaded, leaving other associations unaffected.
Trade-offs and limits
strict_loading is intentionally narrow. It guards association lazy loading only. It will not flag N+1s created by iterating and calling a separate model, or by using select with missing columns.
Enabling it globally changes the failure mode of the app. Existing code that works today will raise tomorrow. Plan a remediation pass and keep the setting off in production until violations are cleared.
Configuration names differ across Rails versions. strict_loading_by_default and per-association strict_loading: true are stable since 6.1, but log-only modes and config keys can vary. Confirm against the version you run.
Actionable next step: enable strict_loading_by_default in development and test, run your test suite, and open key pages locally. Fix each violation with includes, preload or eager_load as appropriate, then consider a limited production canary only after the suite is clean.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.