Rails 7.1 strict_loading: A Gradual Audit for Lazy Associations
Use Rails 7.1 strict_loading to surface lazy-loaded associations incrementally. Start with per-relation checks, move to per-model enforcement, and opt out intentionally lazy associations.
20 Jul 2025, 15:17 UTC

The problem: silent lazy loads hide performance bugs
In a Rails application, accessing an association that was not eager-loaded issues a separate SQL query. Inside loops, this produces N+1 queries that are easy to miss during code review. Rails 7.1 introduces strict_loading, which raises ActiveRecord::StrictLoadingViolationError instead of silently running that extra query. The practical engineering decision is not whether to use the feature, but how to adopt it in an existing codebase without a disruptive big-bang change.
Desired outcome
Surface every unoptimized lazy load in a controlled order, fix the hottest paths first, and keep intentional lazy loads explicit. You want violations to appear in development and continuous integration, not in production.
Prerequisites
Confirm your Rails version before relying on this feature. Run the following command in your project root:
grep -E '^\s+rails \(' Gemfile.lock
Expected check: the output should show rails (7.1.x) or a newer version. If your application is on an older release, strict_loading is unavailable. You also need a test suite or a development environment where you can exercise the controller actions and service objects that load associations, because the error only appears when the offending code path runs.
Step 1: Audit a single relation
Before changing global configuration, test the behavior on one query. In a controller or Rails console, chain .strict_loading onto a relation:
# app/controllers/users_controller.rb
def show
@user = User.strict_loading.find(params[:id])
end
If the corresponding view calls @user.posts and that association was not eager-loaded, Rails raises ActiveRecord::StrictLoadingViolationError. The backtrace points to the exact line that triggered the lazy load. This scope changes no persistent state, so it is safe for ad-hoc debugging and code review.
Step 2: Enforce strict loading on leaf models
After confirming the error behavior, move to per-model enforcement. Start with models that are rarely loaded in isolation, such as Comment or LineItem. Add the declaration inside the model class:
# app/models/comment.rb
class Comment < ApplicationRecord
self.strict_loading_by_default = true
end
This catches lazy loads when a Comment is fetched directly or nested inside another strict relation. It is safer than global enforcement because only code touching Comment records will raise violations.
Step 3: Handle nested associations explicitly
Strict loading does not automatically propagate through an eager-loaded graph. If you load User.includes(:posts) and later call post.comments, that nested load is only strict if Post or Comment is also strict. For a complete audit, enable strict loading on the leaf model as shown above, or eager-load the full path:
User.includes(posts: :comments).find(params[:id])
If a nested association is intentionally lazy, opt out on the association definition rather than hiding the decision in controller code:
class Post < ApplicationRecord
has_many :comments, strict_loading: false
end
Use this opt-out sparingly for optional data, such as a rarely accessed profile image, so the exception remains visible in the model.
Step 4: Run the test suite with global enforcement
Once per-model violations are resolved, turn on global enforcement in your test environment to catch remaining code paths:
# config/environments/test.rb
config.active_record.strict_loading_by_default = true
Run the suite from the project root:
bundle exec rspec
# or
bin/rails test
Expected check: any StrictLoadingViolationError indicates a lazy load that your tests exercise. Fix each one by adding includes, preload, or eager_load to the originating query. Because this setting only changes runtime query behavior, rollback is as simple as removing the configuration line; no database migration is required.
Comparison of enforcement scopes
| Scope | Configuration | Best for |
|---|---|---|
| Relation | .strict_loading | Ad-hoc debugging and code review |
| Model | self.strict_loading_by_default = true | Gradual rollout on leaf models |
| Global | config.active_record.strict_loading_by_default = true | CI and test-suite enforcement |
Expected checks and recovery
- Verify the error: Access an unloaded association in development or a test. You should see
ActiveRecord::StrictLoadingViolationErrorwith a backtrace pointing to the offending call. - Verify the fix: After adding eager loading, re-run the same request or test. The error should no longer appear.
- Recovery: If global enforcement produces too many failures, remove the test-environment configuration and rely on per-model declarations until the violations are fixed.
Limitations
Strict loading is a guardrail, not a complete N+1 detector. It will not flag lazy loads inside eager-loaded collections unless the nested model or association is also strict. It also does not replace query analysis: use EXPLAIN or a query-logging tool to confirm that eager loading actually reduced database round-trips. Behavior may vary across Rails 7.1 patch releases, so verify the exact exception class on your version before writing automated rescues.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.