The N+1 problem is when your code runs one query for a list and then one extra query per item; eager loading with with() fetches all the related data up front in a single extra query.
πImagine buying ingredients for 50 recipes by driving to the shop, buying one item, driving home, and repeating 50 times. Eager loading is writing one shopping list and making a single trip.
The #1 Laravel performance bug
It hides in innocent code
{{ $post->author->name }} looks harmless, but in a loop it fires a query every single time.
It grows with your data
Fast with 10 test posts, painfully slow with 1,000 real ones. The bug appears in production, not on your laptop.
The fix is one word
Adding with('author') usually turns hundreds of queries into two.
Counting the queries
Say the blog home page lists 50 posts and shows each post's author. Here's what reaches the database:
WHERE IN query.- Lazy loading
- Eloquent's default: a relationship is queried the first time you access it, one model at a time.
- Eager loading
- Telling Eloquent in advance which relationships you'll need, so it loads them for the whole collection at once.
- N+1
- 1 query for the list + N queries for the related data, where N is the number of items.
Find it, fix it, prevent it
-
Recognise the N+1 pattern
The controller fetches posts. The view loops and touches a relationship on each one. Nothing looks wrong.
$posts = Post::latest()->take(50)->get(); // 1 query return view('posts.index', compact('posts'));@foreach ($posts as $post) <h2>{{ $post->title }}</h2> <p>by {{ $post->author->name }}</p> {{-- +1 query each time --}} @endforeachTo see it, install Laravel Debugbar (
composer require barryvdh/laravel-debugbar --dev) and open the Queries tab, or use Laravel Telescope. Look for the same query repeated many times. -
Fix it with
with()Name the relationships you'll use. You can load several, and nested ones with dot notation.
$posts = Post::with(['author', 'category', 'comments.author']) ->latest() ->take(50) ->get();That's 1 query for posts + 1 each for authors, categories, comments and comment authors: 5 queries total, no matter how many posts there are.
To load only some columns, list them (always include the keys):
with('author:id,name'). -
Already have the models? Use
load()with()goes on the query.load()does the same thing to models or a collection you already fetched, for example a route-model-bound post.public function show(Post $post) { $post->load('comments.author'); return view('posts.show', compact('post')); }loadMissing()only loads relationships that aren't loaded yet. -
Need just a number? Use
withCount()Showing β12 commentsβ doesn't need every comment row.
withCount()adds a subquery and gives you a{relation}_countattribute.$posts = Post::with('author')->withCount('comments')->get();<span>{{ $post->comments_count }} comments</span>Avoid
$post->comments->count()in a loop (loads every comment) and$post->comments()->count()(one query per post). There's alsowithSum,withAvg,withMaxandwithExists. -
Make lazy loading throw an error in development
Model::preventLazyLoading()turns every accidental lazy load into an exception, so you catch N+1 bugs the moment you write them.use Illuminate\Database\Eloquent\Model; public function boot(): void { Model::preventLazyLoading(! $this->app->isProduction()); }Passing
! isProduction()keeps production users safe from crashes while developers see the error immediately.Model::shouldBeStrict()turns this on together with the other strict checks.
Avoid this, do that
$posts = Post::all();
// view uses $post->author and $post->category
With 200 posts this runs 401 queries.
$posts = Post::with(['author', 'category'])->get();
3 queries, whether you have 200 posts or 20,000.
{{ $category->posts->count() }} posts
Loads every post into memory just to count them.
Category::withCount('posts')->get();
// then: {{ $category->posts_count }}
The database does the counting, in one query.
If a relationship is needed almost every time, add protected $with = ['author']; to the model. Use it sparingly, because it applies to every query on that model.
- Seed 50 posts with authors using factories. Render a list showing each author's name.
- Install Debugbar and note the query count. Then add
with('author')and compare. - Show a comment count per post with
withCount('comments'). - Enable
preventLazyLoading, remove thewith(), and read the exception message.
Key takeaways
- N+1 = 1 query for the list + 1 per item for a relationship accessed in a loop.
with()eager loads on a query;load()eager loads on models you already have.- Eager loading uses one
WHERE INquery per relationship, regardless of list size. - Use
withCount()when you only need a number. Model::preventLazyLoading(! app()->isProduction())catches N+1 bugs during development.