Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 098 / 100

Rails strict loading: a Ruby on Rails guide for developers

Catch lazy loading early.

Expona Admin7 min read

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:

ruby
users = User.all
users.each do |user|
  puts user.posts.count
end

This 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:

ruby
users = User.strict_loading.all

users.each do |user|
  puts user.posts.count  # Raises ActiveRecord::StrictLoadingViolationError
end

This 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:

ruby
class Admin::UsersController < ApplicationController
  def index
    @users = User.strict_loading.includes(:profile, :organization).page(params[:page])
  end
end

The 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:

ruby
class User < ApplicationRecord
  has_many :posts
  has_many :comments

  strict_loading_by_default
end

With 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:

ruby
user = User.find(1)
user.posts  # Raises ActiveRecord::StrictLoadingViolationError

You can override this for specific queries using strict_loading(false):

ruby
user = User.strict_loading(false).find(1)
user.posts  # Works fine, but do this sparingly

In 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:

ruby
ActiveRecord::StrictLoadingViolationError: 
  Post was not eagerly loaded with User

This 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:

ruby
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:

ruby
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class

  if Rails.env.development? || Rails.env.test?
    strict_loading_by_default
  end
end

This 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:

ruby
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
end

This 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:

ruby
users = User.strict_loading.includes(:posts).all

users.each do |user|
  puts user.posts.count  # Works; posts already loaded
end

Rails 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:

ruby
# 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:

ruby
users = User.strict_loading.includes(:posts, :profile).all
# or
users = User.strict_loading.includes(posts: :comments, profile: :avatar).all

In 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:

ruby
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_default on models or use strict_loading on 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles