35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails Strict Loading: How to Catch N+1 Queries Before They Hit Production

Rails Strict Loading: How to Catch N+1 Queries Before They Hit Production

TTB Software
rails
Enable strict loading in Rails 6.1+ to detect and prevent N+1 queries at development time. Configuration options, per-model and global setup, and handling the exceptions that pop up.

N+1 queries are the performance bug you don’t notice until your database is on fire. Rails has had includes and eager_load forever, but they require you to remember to use them. Strict loading flips that model: lazy loading becomes an error unless you explicitly allow it.

Available since Rails 6.1 and refined through Rails 7 and 8, strict loading catches missing eager loads at development time instead of letting them silently tank your production response times.

How Strict Loading Works

When strict loading is enabled on a record or association, any attempt to lazy-load an association raises ActiveRecord::StrictLoadingViolationError. Instead of silently firing off extra queries, your app tells you exactly where you forgot to eager load.

# Without strict loading — silently fires N+1 queries
posts = Post.all
posts.each { |p| p.comments.count } # N+1, no warning

# With strict loading — raises immediately
posts = Post.strict_loading.all
posts.each { |p| p.comments.count }
# => ActiveRecord::StrictLoadingViolationError:
#    `Post` is marked for strict_loading. The Comment association
#    named `:comments` cannot be lazily loaded.

That error tells you exactly which model and which association caused the problem. Fix it by adding the eager load:

posts = Post.strict_loading.includes(:comments).all
posts.each { |p| p.comments.count } # Single query, no error

Three Ways to Enable It

1. Per-Query with .strict_loading

The most targeted approach. Add it to specific queries you know are performance-sensitive:

# Only this query enforces strict loading
@dashboard_posts = Post.strict_loading
                       .includes(:author, :comments, :tags)
                       .where(published: true)
                       .limit(50)

This is useful for controller actions that render complex views where N+1 bugs tend to hide.

2. Per-Model as the Default

Set it on the model class to enforce strict loading on every query for that model:

class Post < ApplicationRecord
  self.strict_loading_by_default = true

  belongs_to :author
  has_many :comments
  has_many :tags, through: :taggings
end

Now Post.all returns strict-loading records automatically. Every controller and background job that touches Post must explicitly eager load associations or face the exception.

This is aggressive. It works well for models you know are frequently involved in N+1 issues — your core business objects with lots of associations.

3. Per-Association

Mark individual associations as strict:

class Post < ApplicationRecord
  has_many :comments, strict_loading: true
  has_many :audit_logs  # This one can still lazy load
end

This is the most granular option. Use it when one specific association is expensive (like a has_many with thousands of records) but other associations on the same model are fine to lazy load.

Global Strict Loading in Development

The most practical setup for most teams: enable strict loading globally in development and test, but leave it off in production. This catches N+1 bugs during development without risking exceptions in production if something slips through.

In Rails 6.1 and 7.x, there’s no single config toggle for this. The common approach is an initializer:

# config/initializers/strict_loading.rb
if Rails.env.local? # .local? covers development + test in Rails 7.1+
  ActiveRecord::Base.strict_loading_by_default = true
end

Rails 8 added config.active_record.strict_loading_by_default to the framework configuration, so you can set it directly:

# config/environments/development.rb
config.active_record.strict_loading_by_default = true

Strict Loading Mode: :log vs :raise

Rails 7.1 introduced strict_loading_mode, which lets you log violations instead of raising:

# Log instead of explode
ActiveRecord::Base.strict_loading_mode = :log

With :log mode, violations appear in your Rails log as warnings but don’t halt execution. This is useful during a migration period when you’re enabling strict loading on an existing codebase with hundreds of queries — you can see all the violations at once instead of fixing them one at a time.

# In your logs:
# WARN -- : `Post` is marked for strict_loading.
# The Comment association named `:comments` cannot be lazily loaded.

Once you’ve cleaned up the violations, switch to :raise for hard enforcement.

Handling the Transition

Enabling strict loading on a large codebase will surface dozens or hundreds of violations. Here’s a practical approach:

Step 1: Enable globally in development with :log mode. Run your test suite and note every violation.

Step 2: Sort violations by frequency. The queries that fire on every request matter more than the ones in a rarely-used admin panel.

Step 3: Fix the high-frequency ones first. Most fixes are straightforward includes calls in controllers or scopes:

# Before
def index
  @posts = Post.where(published: true)
end

# After
def index
  @posts = Post.includes(:author, :tags)
               .where(published: true)
end

Step 4: For queries where eager loading doesn’t make sense (because the association is conditionally accessed), use strict_loading!(false) to explicitly opt out:

post = Post.strict_loading.find(params[:id])
post.strict_loading!(false) # Explicitly allow lazy loading for this record

Document why you opted out. A comment explaining “lazy-loaded because X is only accessed in 5% of requests” prevents the next developer from re-enabling it without understanding the tradeoff.

Step 5: Switch to :raise mode once violations are manageable.

Common Patterns That Break Under Strict Loading

A few patterns need adjustment:

Serializers that traverse associations. If you use ActiveModel Serializers or Blueprinter, they often walk through associations. You need to eager load everything the serializer touches at the query level.

Callbacks accessing associations. An after_save that touches post.author will raise if the post was loaded with strict loading. Either eager load before the callback fires or use strict_loading!(false) explicitly.

Delegation through associations. delegate :name, to: :author on a strict-loaded Post will raise when :name is called. The fix is the same: make sure author is eager loaded wherever this delegation is used.

View helpers and partials. A partial that calls post.comments.count somewhere buried three levels deep is a classic N+1 source. Strict loading makes these immediately visible.

Performance Impact of Strict Loading Itself

Strict loading adds negligible overhead. It sets a flag on the record object and checks that flag when an association proxy is accessed. The check is a simple boolean test. In benchmarks on Rails 8.0 with Ruby 3.3, enabling strict loading on 10,000 records added less than 1ms total overhead.

The performance you gain from catching missing eager loads far outweighs this cost. A single N+1 loop over 100 records can add 200-500ms to a request. Strict loading prevents that.

Strict Loading and has_many Counter Caches

One gotcha: if you use counter caches to avoid counting associated records, strict loading won’t interfere with them. Counter cache columns live on the parent model, so post.comments_count doesn’t trigger an association load. But post.comments.size might — size checks whether the association is loaded and falls back to a COUNT query if it isn’t, which strict loading will flag.

Use post.comments_count (the counter cache column) instead of post.comments.size under strict loading.

How It Fits with Other Performance Tools

Strict loading works well alongside other N+1 detection tools:

  • Bullet gem: Bullet detects N+1 queries at runtime and suggests eager loads. Strict loading enforces them. Use Bullet in development for suggestions, strict loading in tests for enforcement.
  • Prosopite: Similar to Bullet but with a different detection approach. Can run in production with minimal overhead.
  • db:explain and query logs: Good logging setup helps you spot the queries that strict loading flagged.

The combination of Bullet for discovery and strict loading for enforcement catches more N+1 issues than either tool alone.

FAQ

Does strict loading work with preload and eager_load or only includes?

All three work. includes, preload, and eager_load all satisfy strict loading requirements. The difference between them is how Rails constructs the SQL (separate queries vs. LEFT OUTER JOIN), but strict loading only cares whether the association data is already loaded when you access it.

Can I use strict loading in production?

You can, but most teams don’t. If a lazy load slips through in production, you get a 500 error instead of a slow page. The safer pattern is :raise in development/test and either :log or disabled in production. If you’re confident in your test coverage, :raise everywhere is an option.

How does strict loading interact with includes on nested associations?

Nested eager loading works as expected. If you includes(comments: :author), both post.comments and comment.author are satisfied. But if you only includes(:comments), accessing comment.author on a strict-loaded record will still raise.

What’s the difference between strict_loading and strict_loading!?

strict_loading is the query method that returns a new relation: Post.strict_loading.all. strict_loading! is the instance method that modifies a record in place: post.strict_loading! enables it, post.strict_loading!(false) disables it. The query method is generally preferred because it’s declarative and doesn’t mutate state.

Does strict loading affect belongs_to associations?

Yes. A belongs_to :author will raise on a strict-loaded record if the author isn’t eager loaded. This trips up some teams because belongs_to loads are usually fast (single row lookup by primary key), so they seem harmless. But 100 of them in a loop is still 100 queries. Strict loading correctly flags this.

T

About the Author

Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation