This innocent-looking Laravel loop runs 101 database queries. It's called the N+1 problem, and it's the most common reason Laravel apps get slow. I measured everything below with Eloquent on a real database, re-checked on Laravel 13.
Video version:
The innocent loop
$posts = Post::all(); // 1 query foreach ($posts as $post) { echo $post->user->name; // +1 query PER post! } // 100 posts = 101 queries Post::all() is one query. But each time you touch $post->user, Laravel lazy loads the author with another query. That's one query for the posts, plus one per post: N+1.
What the database actually sees
Turn on the query log (DB::enableQueryLog()) or install Laravel Debugbar:
select * from "posts" select * from "users" where "users"."id" = 1 limit 1 select * from "users" where "users"."id" = 2 limit 1 select * from "users" where "users"."id" = 3 limit 1 -- … 97 more … On a real server, every one of those is a network round trip to the database.
The fix: eager load with with()
$posts = Post::with('user')->get(); foreach ($posts as $post) { echo $post->user->name; // no extra query } The loop doesn't change at all. Laravel loads the posts, then loads every author in one query, and matches them up in memory:
select * from "posts" select * from "users" where "users"."id" in (1, 2, 3, …, 100) That's 2 queries instead of 101, and it stays at 2 whether you have 10 posts or 10,000.
More eager-loading tricks
// several relations at once (3 queries) Post::with(['user', 'comments'])->get(); // nested: users → posts → comments (3 queries) User::with('posts.comments')->get(); // only need a number? (1 query) Post::withCount('comments')->get(); // $post->comments_count withCount() adds a comments_count attribute using a subquery, so you don't load a single comment model just to count them.
Make Laravel catch N+1 for you
In AppServiceProvider::boot():
use Illuminate\Database\Eloquent\Model; public function boot(): void { Model::preventLazyLoading(! app()->isProduction()); } Now any lazy load during development throws:
Illuminate\Database\LazyLoadingViolationException: Attempted to lazy load [user] on model [Post] but lazy loading is disabled. It even tells you exactly which relation to eager load. The ! app()->isProduction() part keeps production from crashing over a missed eager load.
Cheat sheet
| Relation inside a loop? | N+1 risk |
| Spot it | query log or Laravel Debugbar |
| Fix it | Post::with('user'): 2 queries, not 101 |
| More relations | with(['a', 'b']), with('posts.comments') |
| Only need a count | withCount() |
| Catch it automatically | Model::preventLazyLoading() in dev |
How many queries does your slowest page run? Check it. 👀
I post one tested coding tip every day on YouTube at Refactorly. The 30-second version of this one is here.
