Overusing with() and Missing the Real N+1 in Laravel
You added with() everywhere. Telescope still shows 200 queries on a page that should have 4.
That is the trap. with() solves one class of N+1. Developers treat it like a blanket fix, stop looking, and ship the problem anyway - just a slightly different version of it.
This post is about the N+1 patterns that with() does not touch, and how to actually find and close them.
Why with() Earns False Confidence
Eager loading via with() prevents the classic loop problem:
// Bad: fires one query per post
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // SELECT * FROM users WHERE id = ?
}
// Better: two queries total
$posts = Post::with('author')->get();
That fix is real. Two queries instead of N+1. Good.
But with() only works when you know the relationship ahead of time, at the point you build the initial query. The moment the relationship is accessed somewhere else in the request lifecycle, you are back to firing individual queries.

The Four Places with() Cannot Help You
1. Lazy loading triggered inside a service or helper
You eager-load on the controller. Then you pass the collection into a service class that calls a different relationship you forgot to include.
// Controller
$orders = Order::with('customer')->get();
OrderExportService::handle($orders);
// Inside OrderExportService
foreach ($orders as $order) {
$order->items->each(...); // 'items' was never eager-loaded
}
Each $order->items fires a query. You have N+1. Telescope will show it. Most developers do not look.
The fix: audit every relationship touched downstream from your initial query. Add them all to with(), or use loadMissing() before the service runs.
2. Accessors and computed attributes that hit the database
A custom accessor looks like a property access. It reads like safe cached data. It is not.
// App\Models\Product
public function getIsPopularAttribute(): bool
{
return $this->orders()->count() > 100; // COUNT query per product
}
Now iterate over 50 products and call $product->is_popular in a Blade loop. That is 50 extra queries. with() does not know this accessor exists. There is no relationship name to eager-load.
The fix: push this logic to a query scope with a sub-select, or store it as a computed column updated via an observer or scheduled job.
3. Polymorphic relationships loaded without morphMap
Polymorphic eager loading works, but without a morphMap, Laravel resolves the morph type using the full class name stored in the database. If your morph types are inconsistent across environments, resolution silently falls back to individual queries.
More commonly, developers eager-load one side of the polymorphic relation and forget the other:
// Only loads commentable for Comment, not the nested author on each commentable
$comments = Comment::with('commentable')->get();
foreach ($comments as $comment) {
echo $comment->commentable->author->name; // fresh query per commentable
}
The fix: chain nested eager loading - with('commentable.author').
4. Conditional relationships inside loops
This one is subtle. You check a condition, then load a relationship based on it.
foreach ($users as $user) {
if ($user->role === 'admin') {
$user->load('permissions'); // individual query per admin user
}
}
with() on the parent query cannot conditionally eager-load per row. You end up with one query per matching row.
The fix: use with() with a constrained sub-query, or partition the collection before the loop and loadMissing() on the subset.
$users->filter(fn($u) => $u->role === 'admin')->loadMissing('permissions');
How to Actually Find These
Running the app and hoping is not a strategy. Use these tools in combination:
- Laravel Telescope - query panel. Sort by count. Anything above 10 queries for a single request needs explaining.
- Laravel Debugbar - shows query count and caller stack in local dev. The caller column tells you which file and line triggered the query.
DB::listen()- add it to a service provider in local env. Log every query with its caller. Useful for finding queries buried inside services or jobs.preventLazyLoading()- the single most useful method for catching this in CI:
// AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());
This throws a LazyLoadingViolationException any time a relationship is accessed without being eager-loaded. Run your test suite with this enabled. Every violation surfaces immediately, before it reaches production.

A Pattern Worth Building Into Every Project
Before shipping any list endpoint, do a quick query audit:
- Open Telescope or Debugbar on the route.
- Count queries. Write the number down.
- For any count above (relationships + 1), trace each extra query to its caller.
- Fix at the source - eager load, sub-select, or cache.
- Recount. Target is relationships + 1, ideally.
For a typical paginated list with two relationships, you should see 3 queries: one for the list, one per relationship. If you see 30, you have at least one unaddressed N+1.
This takes 10 minutes per endpoint. It saves hours of production debugging later.
The Broader Point
with() is a tool for a specific problem. Treating it as a general-purpose performance fix leads to over-eager loading (fetching columns and relationships you never use) alongside under-eager loading (missing the ones that actually matter).
The real discipline is: instrument every list query, read the output, and fix what the output tells you - not what you assume you already fixed.
I have seen production Laravel apps with with() on every query and still 300+ queries per page load because nobody checked what the accessors, services, and polymorphic chains were doing underneath.
preventLazyLoading() in your test suite is the cheapest guard you have. Turn it on today.

Quick Reference: N+1 Fix Cheatsheet
| Scenario | Root Cause | Fix |
|---|---|---|
| Loop over collection, access relation | Missing with() | Add relation to with() |
| Accessor fires a query per row | DB call inside attribute method | Sub-select or computed column |
| Polymorphic chain, nested relation | Incomplete eager load | with('morphable.nestedRelation') |
| Conditional relation load inside loop | Per-row load() | Partition + loadMissing() on subset |
| Service/helper accesses unexpected relation | Eager load scope too narrow | Audit downstream callers, extend with() |
Spent enough time chasing phantom N+1s in codebases that looked "fixed" on the surface. preventLazyLoading() caught more real issues in the first test run than a week of manual Telescope review.
What is the most unexpected place you found an N+1 hiding in a Laravel app?
Frequently asked questions
No. with() prevents N+1 only for relationships you explicitly name at the point of the initial query. Relationships accessed later inside services, accessors, or conditional loops still fire individual queries. You need to audit the full call chain, not just the controller.
It is an Eloquent method introduced in Laravel 8.x that throws a LazyLoadingViolationException whenever a relationship is accessed without being eager-loaded. Enable it in non-production environments (ideally all environments including CI) so violations surface during development and testing rather than in production.
If an accessor fires a database query per model instance, with() cannot help because there is no named relationship to eager-load. The fix is to push the logic into a sub-select on the query itself (using withCount, withSum, or a raw sub-select), or store the computed value as a database column updated by an observer or scheduled job.
Use Laravel Debugbar for quick visual query counts and caller stacks during local development. Use Laravel Telescope for a persistent query log you can review across requests. Add DB::listen() in your AppServiceProvider to log every query with its origin file and line for deep audits.
Partition the collection first, then call loadMissing() on the subset that needs the relationship. For example: $users->filter(fn($u) => $u->role === 'admin')->loadMissing('permissions'). This fires one query for the whole subset rather than one query per matching row.
Enjoyed this article?
Get notified when I publish new posts on SaaS, Laravel, and remote engineering.



