Your Laravel app feels fast on your laptop. Then real data arrives, a page that showed 10 posts now shows 200, and suddenly response times climb from milliseconds to seconds. In most Laravel projects I've audited, the culprit is the same: the N+1 query problem.
The good news is that Laravel gives you a clean fix built right into Eloquent: eager loading. In this guide you'll learn what N+1 queries are, how to spot them, and how to eliminate them with with(), load(), withCount() and a few lesser-known techniques that keep your database calm under load.
What is the N+1 query problem?
The N+1 problem happens when your code runs one query to fetch a list of records, then one extra query for each record to fetch a related model.
Here's the classic example. A blog listing page shows each post with its author's name:
// Controller
$posts = Post::all();
// Blade view
@foreach ($posts as $post)
<h2>{{ $post->title }}</h2>
<p>By {{ $post->author->name }}</p>
@endforeach
It looks harmless. But Eloquent relationships are lazy loaded by default, so every time the loop touches $post->author, Laravel fires a new query:
select * from posts;
select * from users where id = 1;
select * from users where id = 2;
select * from users where id = 3;
-- ...one more for every post
With 100 posts, that's 101 queries for a single page. Add comments, categories and tags to the same loop and you can easily cross 400 queries per request.
Why N+1 queries hurt performance
Each query is cheap on its own. The cost is in the round trips. Every query means:
- A network round trip between your app server and the database
- Query parsing and execution on the database side
- PHP hydrating a new Eloquent model object
Multiply that by hundreds and you get slow pages, higher database CPU, more connections held open, and a hosting bill that grows faster than your traffic. On a busy POS or ERP dashboard, it's the difference between a screen that loads instantly and one that staff complain about every day.
The fix: eager loading with with()
Eager loading tells Eloquent to fetch the related models up front, in a single extra query, instead of one at a time:
$posts = Post::with('author')->get();
Now Laravel runs just two queries, no matter how many posts there are:
select * from posts;
select * from users where id in (1, 2, 3, 4, 5, ...);
Eloquent then matches each author to its post in memory. Your Blade view doesn't change at all. You went from N+1 queries to 2 with one method call.
Eager loading multiple and nested relationships
Real pages need more than one relationship. Pass an array:
$posts = Post::with(['author', 'category', 'tags'])->get();
For relationships of relationships, use dot notation:
// Posts → comments → the user who wrote each comment
$posts = Post::with('comments.user')->get();
Each level adds one query, not one per row. Four relationships means roughly five queries total, whether you load 10 posts or 10,000.
Load only the columns you need
Eager loading reduces query count. Selecting specific columns reduces the size of each query's result:
$posts = Post::with('author:id,name,avatar')->get();
Always include the key Eloquent needs to match records, usually id (and the foreign key on belongsTo and hasMany relations). Leave it out and your relationships will silently come back empty.
Constrained eager loading
Sometimes you only want part of a relationship, like approved comments or the latest few:
$posts = Post::with([
'comments' => fn ($query) => $query
->where('approved', true)
->latest()
->limit(5),
])->get();
This keeps the payload small and the logic in one readable place.
Count related records without loading them
If your view only shows "12 comments", don't load every comment just to call count() on it. Use withCount():
$posts = Post::withCount('comments')->get();
// In Blade
{{ $post->comments_count }} comments
Laravel adds a subquery and returns the number as an attribute. There are also withSum(), withAvg(), withMin(), withMax() and withExists() for the same purpose, which are perfect for dashboards and reports.
Lazy eager loading with load() and loadMissing()
Already have a collection and decided later that you need a relationship? Use load():
$posts = Post::all();
if ($request->boolean('with_authors')) {
$posts->load('author');
}
loadMissing() does the same but skips relationships that are already loaded, which is useful in shared services or API resources where you're not sure what the caller already fetched.
How to detect N+1 queries in Laravel
You can't fix what you can't see. These are the tools I rely on.
1. Prevent lazy loading in development. This is the single most effective habit you can adopt. Add it to AppServiceProvider:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
Any lazy-loaded relationship now throws an exception locally, so N+1 bugs get caught while you're writing the code instead of after users feel them. In production it stays off, so nothing breaks for real users. You can also log violations instead of throwing:
Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
logger()->warning("Lazy loaded [{$relation}] on " . get_class($model));
});
For a stricter setup, Model::shouldBeStrict() also enables protections against silently discarded attributes and accessing missing attributes.
2. Laravel Debugbar. The barryvdh/laravel-debugbar package shows every query per request and highlights duplicates. If you see the same query repeating with different IDs, you've found an N+1.
3. Laravel Telescope. Telescope records queries per request and is great for spotting slow or repeated queries across your whole app, including queued jobs.
4. Query logging in tests. For critical endpoints, assert the query count so regressions fail your test suite:
DB::enableQueryLog();
$this->get('/posts')->assertOk();
$this->assertLessThan(10, count(DB::getQueryLog()));
Eager loading in Livewire and API resources
N+1 issues hide easily in two places in modern Laravel apps.
Livewire components re-render often, so a lazy-loaded relationship inside a component's view can fire queries on every interaction. Eager load inside your query, for example in a computed property:
#[Computed]
public function orders()
{
return Order::with(['customer', 'items.product'])
->latest()
->paginate(20);
}
API Resources often access relationships inside toArray(). Use whenLoaded() so the resource only includes a relationship if the controller eager loaded it, instead of triggering a query:
'author' => new UserResource($this->whenLoaded('author')),
Should you use the $with property on models?
You can make a relationship always eager load by adding it to the model:
protected $with = ['author'];
Use this carefully. It runs on every query for that model, including places where you don't need the relationship, which can create the opposite problem: loading data nobody uses. I prefer explicit with() calls in the query where the data is actually needed. Reserve $with for relationships that are genuinely required nearly everywhere.
Recent Laravel versions also offer automatic relationship loading (Model::automaticallyEagerLoadRelationships() globally, or ->withRelationshipAutoloading() on a collection). It's a handy safety net, but explicit eager loading remains clearer and easier to reason about in code review.
Eager loading best practices checklist
- Turn on
Model::preventLazyLoading()outside production on every project - Eager load every relationship you access inside a loop
- Select only the columns you need, and always include the keys
- Use
withCount()and other aggregates instead of loading full collections to count them - Constrain eager loads when you only need a subset of related records
- Combine eager loading with
paginate()so you never load more rows than you show - Use
whenLoaded()in API Resources - Watch query counts in Debugbar or Telescope during development
Frequently asked questions
What is eager loading in Laravel?
Eager loading is an Eloquent feature that loads related models in advance using a small, fixed number of queries, instead of querying the database each time you access a relationship. You use it with methods like with(), load() and loadMissing().
What is the difference between lazy loading and eager loading?
Lazy loading fetches a relationship only when you first access it, which causes one query per record inside loops. Eager loading fetches all related records for a collection at once, usually with a single where in query.
Is eager loading always better?
Not always. If you load a relationship you never use, you waste memory and query time. Eager load what the page actually displays, and nothing more.
How do I find N+1 queries in an existing project?
Enable Model::preventLazyLoading() in your local environment, then click through your app. Every exception points to an N+1 problem. Laravel Debugbar and Telescope help you find the ones with the biggest impact.
Final thoughts
The N+1 query problem is one of the most common performance issues in Laravel applications, and also one of the easiest to fix. A single with() call can turn hundreds of queries into two, and one line in your service provider makes sure new ones never slip through again.
If your Laravel app is slowing down as your data grows, start by checking its query count. Need help auditing or optimizing a Laravel, Livewire or TALL stack project? Get in touch, and let's make it fast.
Let's talk 