Avoid the N+1 Queries That Strict Mode Misses in Laravel
September 17, 2026 • 3 min read
Most admin panels have a customers page with an “Orders” column showing how many orders each customer has placed. You eager load the orders so the page doesn’t run a query per row, and you might have strict mode turned on in your AppServiceProvider to catch any you missed in development:
Model::shouldBeStrict(! $this->app->isProduction());The page then does something like this:
$customers = Customer::query()->with('orders')->get();
foreach ($customers as $customer) {
$orderCount = $customer->orders()->count();
}The eager load doesn’t help here, and strict mode doesn’t throw. I ran it with 50 customers and 500 orders between them, and the loop ran 52 queries: two for the eager load, then one more for every customer.
Let’s look at why the eager load is ignored, and what to write instead.
Comparing the property and the method
Here’s each common pair, run against the same 50 customers after with('orders'):
| Code | Queries |
|---|---|
$customer->orders->count() | 2 |
$customer->orders()->count() | 52 |
$customer->orders->where('status', 'paid') | 2 |
$customer->orders()->where('status', 'paid')->get() | 52 |
$customer->orders->isNotEmpty() | 2 |
$customer->orders()->exists() | 52 |
$customer->orders->first() | 2 |
$customer->orders()->first() | 52 |
Each pair does the same job, and the version with parentheses adds a query per customer. The count() pair is the one I’d expect to find in most codebases, since the two versions are two characters apart and both look correct.
Why the parentheses matter
$customer->orders is a property. It returns the collection that with('orders') already loaded onto the model.
$customer->orders() is a method. It returns a new HasMany relation built from the customer’s id, and calling count(), exists(), first() or get() on it always runs a query. The eager-loaded orders aren’t used.
Both are valid. When you need something the eager load couldn’t have covered, like a different filter or sort order, the method is the right choice.
Why strict mode doesn’t catch it
Laravel has two settings that deal with N+1 queries. Model::shouldBeStrict() throws a LazyLoadingViolationException when you read a relationship that wasn’t loaded. Automatic eager loading, added in Laravel 12.8 in a pull request from @litvinchuk, loads that relationship for the whole collection instead.
Neither of them changes anything for the method:
shouldBeStrict() | automaticallyEagerLoadRelationships() | |
|---|---|---|
$customer->orders, never eager loaded | throws | 2 queries |
$customer->orders()->count() after with('orders') | 52 queries | 52 queries |
Their N+1 checks only run when you read a relationship property that hasn’t been loaded. Calling orders() and running a query on it is normal code, so there’s nothing for them to flag.
Using the loaded data instead
If the orders are already loaded, use the property: $customer->orders->count() instead of $customer->orders()->count().
If you only need the count, withCount() adds it to the customers query, so the whole page is a single query:
$customers = Customer::query()->withCount('orders')->get();
foreach ($customers as $customer) {
$orderCount = $customer->orders_count;
}If you only need some of the orders, constrain the eager load instead of filtering in the loop:
use Illuminate\Contracts\Database\Eloquent\Builder;
$customers = Customer::query()
->with(['orders' => fn (Builder $query) => $query->where('status', 'paid')])
->get();That loads the 250 paid orders. Loading everything and filtering in PHP is also two queries, but it loads all 500 orders and throws half of them away.
Finding them in your codebase
Since no setting catches this, you have to search for it. Look for ->orders()->, or the names of your own relationships, and check whether the parent query eager loaded that relationship. If it did, the call is either deliberate or a wasted query per row.
A query counter in local development makes them easier to spot. Laravel Debugbar and Telescope will both show 52 queries where you expected 2.
So once a relationship is eager loaded, read it through the property, and keep the method for queries the eager load couldn’t have covered. Strict mode won’t tell you which one you wrote.