N+1 is what happens when you make one query to fetch a list (say, User::all()) and then make one extra query per item inside a loop. Instead of 1 query, the database sees N+1.
What it looks like
The classic shape:
$users = User::all();
foreach ($users as $user) {
echo $user->profile->bio;
}
With 50 users this code makes 51 queries: one for users, plus 50 lookups against profiles.
The shape of the problem is easier to see as a picture:
How to detect it
Three approaches, simple to systematic:
- Laravel Telescope — in development, it flags queries with an "N+1" badge.
DB::listen()inAppServiceProvider— log every query and grep the output.Model::preventLazyLoading()— throws when any relation is loaded lazily, perfect for tests.
A quick comparison of when each is useful:
| Tool | When to reach for it | Catches | Cost |
|---|---|---|---|
| Telescope | Local dev, exploratory | Most N+1 patterns | Slows the app a little |
DB::listen() |
Targeted profiling, CI | Anything you log | Manual log review |
preventLazyLoading() |
Test suite | All lazy loads, by error | Forces you to fix immediately |
How to fix it
Eager loading
The simplest path is with():
$users = User::with('profile')->get();
Now there are exactly two queries: users and profiles WHERE user_id IN (...).
Lazy eager loading
If the collection is already loaded and you only later realize you need a relation:
$users->load('profile');
Constraining an eager load
You can narrow an eager-loaded relation so you do not pull more than you need:
$users = User::with(['posts' => function ($q) {
$q->where('published', true)->latest()->limit(5);
}])->get();
A real-world example
Here is a service class I refactored last year that started life as a slow report and ended up as a single-digit-millisecond endpoint after the eager-loading pass. The key change is two lines, but the surrounding shape is what makes it survive future contributors:
<?php
namespace App\Services;
use App\Models\Author;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
final class AuthorReportService
{
public function __construct(private readonly int $publishedSince)
{
}
public function build(): Collection
{
return Author::query()
->whereHas('posts', fn (Builder $q) => $q->where('published_at', '>=', $this->publishedSince))
->with([
'profile',
'posts' => fn (Builder $q) => $q
->where('published_at', '>=', $this->publishedSince)
->latest('published_at'),
'posts.tags',
'posts.comments' => fn (Builder $q) => $q->where('approved', true),
])
->withCount(['posts as published_posts_count' => fn (Builder $q) => $q->whereNotNull('published_at')])
->orderByDesc('published_posts_count')
->get()
->map(fn (Author $author) => [
'id' => $author->id,
'name' => $author->name,
'bio' => $author->profile?->bio,
'post_count' => $author->published_posts_count,
'recent' => $author->posts->take(3)->map(fn ($post) => [
'id' => $post->id,
'title' => $post->title,
'tags' => $post->tags->pluck('slug')->all(),
'replies' => $post->comments->count(),
]),
]);
}
}
The interesting parts:
- One
whereHas()filters authors at the SQL level — no in-PHP filtering after the fetch. - The
with(['posts' => …, 'posts.tags', 'posts.comments' => …])chain is the entire point: nested relations + per-relation constraint. withCountprojects a synthetic column we can sort by without an extra query.- The shape of the closure inside
with()matters — pullingpostsand its tags and its filtered comments in one pre-fetch pass.
Take this from ~120 queries on 30 authors to 4. That is the entire optimization, and it is invisible to the controller calling ->build().
The point is not the cleverness of any single line. It is that "fix N+1 once, never think about it again at this layer" is a feature of how you organise the data-loading edge of the codebase, not a one-time tweak.
Summary
N+1 is the most expensive mistake I see in Laravel projects. Eager loading covers ~80% of cases; preventLazyLoading() in tests catches the rest before they ship. The remaining 20% — top-N per group, deep nested relations, polymorphic edges — are where the Eloquent docs become required reading.




