What is the N+1 problem?
The N+1 problem happens when your code fires one database query to fetch a list of records, then one additional query per record to load a related piece of data. If you have 100 posts, that’s 101 queries hitting your database for work that should take 2.
Here’s the classic example. You have a posts table and a users table. Every post belongs to an author (a user). You want to show a list of posts with their authors’ names:
$posts = Post::all(); // Query 1: SELECT * FROM posts
foreach ($posts as $post) {
// Runs a NEW query for EVERY post!
// SELECT * FROM users WHERE id = ?
echo $post->author->name;
} With 50 posts, that innocent-looking loop fires 51 queries. With 500 posts, 501. The number of queries grows linearly with your data, which is exactly why apps that feel fine in development collapse under real-world load.
Why is it invisible
The N+1 problem doesn’t produce errors. Eloquent happily fires each query, Laravel returns the right data, and everything looks correct. The only symptom is slowness — and in development with 10 seed records, you won’t even notice it.

The fix: eager loading with with()
The solution is one word: with(). Instead of letting Eloquent lazily discover what relationships it needs inside the loop, you tell it upfront which relationships to load. Laravel then fetches everything in exactly two queries, no matter how many records you have.
// One call to with() is all it takes
$posts = Post::with('author')->get();
foreach ($posts as $post) {
// No query here. The author is already in memory.
echo $post->author->name;
}
That’s it. One change, from Post::all() to Post::with('author')->get(), and you go from 1+N queries down to exactly 2 — no matter the size of the dataset.
Eager loading doesn’t change what data you get back — it only changes whenand how the data is fetched. Your loop still accesses $post->author->name the same way.
The difference is that by the time the loop starts, all the authors are already sitting in PHP memory.

What happens under the hood
Understanding the two-query mechanism makes the fix click. When you call Post::with('author')->get(), Laravel does this internally:
Step 1 — It runs SELECT * FROM posts and holds all the post records.
Step 2 — It scans those records and collects every unique author_id value. If posts 1, 2, and 3 have author_ids of 7, 12, and 7, it collects [7, 12] (no duplicates).
Step 3 — It fires one query using SQL’s IN clause: SELECT * FROM users WHERE id IN (7, 12). This is equivalent to writing multiple WHERE id = ? conditions joined by OR, but in a single round-trip.
Step 4 — It matches each author back to their posts in PHP memory. No more database involvement.

Notice that even though posts 1 and 3 share the same author_id: 7, Laravel is smart enough to deduplicate — it only fetches that author once.
Nested relationships
with() handles deeply nested relationships using dot notation. If a post belongs to an author, and that author belongs to a company, you can load all three levels in one shot:
// Load posts → authors → companies in 3 queries total $posts = Post::with('author.company')->get(); // Load multiple relationships at once $posts = Post::with(['author', 'tags', 'comments'])->get(); // Mix of both $posts = Post::with([ 'author.company', 'tags', 'comments.user', ])->get(); The number of queries is always equal to the number of distinct relationships being loaded, not the number of records. Loading three separate relationships for 1,000 posts costs exactly 4 queries (1 for posts + 3 for relationships).
Advanced patterns
1. Lazy eager loading
Already have a collection and realise you forgot a relationship? Use load() to eager-load it after the fact, without re-fetching the posts:
$posts = Post::all(); // Already fetched
// Load author relationship on the existing collection
$posts->load('author');
2. Conditional eager loading
You can apply constraints to the eager-loaded relationship — for example, only load published comments:
$posts = Post::with([ 'comments' => function ($query) { $query->where('approved', true) ->latest(); }, ])->get(); 3. Counting without loading
If you only need the count of related records (not the records themselves), withCount() adds a _count attribute without pulling all the data into memory:
$posts = Post::withCount('comments')->get(); foreach ($posts as $post) { echo $post->comments_count; // No extra queries } 4. Enforcing eager loading globally
In Laravel 8+, you can prevent lazy loading entirely in your AppServiceProvider. This throws an exception any time your app tries to lazy-load a relationship in development, forcing you to catch N+1 problems before they reach production:
use Illuminate\Database\Eloquent\Model; public function boot(): void { // Throw an exception when lazy loading in development Model::preventLazyLoading(!app()->isProduction()); } Use with care
preventLazyLoading() is aggressive — it will break any part of your application that relies on lazy loading. Enable it on a feature branch first and fix the errors one by one before committing to your main branch.
Detecting N+1 in your app
The best tools for finding N+1 problems in existing Laravel applications:
| Tool | How it helps | Best for |
|---|---|---|
| Laravel Debugbar | Shows every query fired per request with timing, in a browser toolbar | Development — spot the 50 identical queries instantly |
| Telescope | Laravel’s official debugging dashboard; logs queries, jobs, mail, and more | Local & staging environments |
| DB::listen() | Hook into every query programmatically; log or count them yourself | Custom alerting, CI checks |
| preventLazyLoading() | Throws an exception whenever lazy loading is triggered | Enforcement during development |
The tell-tale sign in Debugbar: you’ll see dozens of identical queries with only the WHERE id = ? value changing. That pattern, repeated N times, is a confirmed N+1.
Summary & cheat sheet
The N+1 problem is one of the most common performance issues in Laravel, but also one of the easiest to fix. Here’s everything you need to remember:

Quick reference
// Single relationship
Post::with('author')->get();
// Multiple relationships
Post::with(['author', 'tags', 'comments'])->get();
// Nested relationship (dot notation)
Post::with('author.company')->get();
// With constraint on the relationship
Post::with(['comments' => fn($q) => $q->where('approved', true)])->get();
// Count without loading
Post::withCount('comments')->get();
// Eager load after the fact
$posts->load('author');
// Prevent lazy loading in dev
Model::preventLazyLoading(!app()->isProduction());
The rule of thumb: If you’re looping over a collection and touching a relationship inside the loop, you almost certainly need with(). When in doubt, open Laravel Debugbar and count the queries. Identical queries firing in sequence is the smoking gun.


