Skip to main content
← Back to articles
Practical guide Laravel development

Stop Guessing Why Your Laravel App Is Slow: A Practical Guide to Killing N+1 Queries

Learn how to detect and fix N+1 query problems in Laravel Eloquent, including the hidden N+1s inside API resources that most tutorials miss.

Birendra Jung Rai 5 min read
Stop Guessing Why Your Laravel App Is Slow: A Practical Guide to Killing N+1 Queries

If your Laravel app feels sluggish under real traffic but flies on your local machine with a handful of seed rows, there's a good chance you're staring at the most common — and most fixable — performance problem in Eloquent apps: the N+1 query.

This isn't a "just use eager loading" one-liner post. We'll look at where N+1 problems actually hide (including the sneaky ones inside API resources and Blade views), how to detect them before your users do, and how to fix them without turning your codebase into a wall of with() calls nobody understands six months from now.

What N+1 Actually Costs You

Here's the classic setup:

$posts = Post::all();

foreach ($posts as $post) {
    echo $post->author->name;
}

One query fetches the posts. Then, for every single post, Eloquent lazily fires a separate query to fetch its author. 50 posts means 51 queries. 500 posts means 501 queries. Your database isn't getting slower — you're just asking it to do 500 extra round trips it never needed to make.

The insidious part is that this code looks completely fine in review. It reads naturally, tests pass, and on a staging database with 10 rows, nobody notices the extra 9 queries. It's only in production, with real data volume, that this quietly becomes the thing eating your response time.

Finding N+1s Before Your Users Do

You don't need to inspect query logs manually forever. A few tools make this almost automatic:

Laravel Debugbar — shows you the exact query count per request in local development. If a page that should run 3 queries is running 340, that's your signal.

composer require barryvdh/laravel-debugbar --dev

Strict mode for lazy loading — this is the one every intermediate Laravel dev should turn on. Add this to your AppServiceProvider:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

With this enabled, any lazy-loaded relationship throws a LazyLoadingViolationException in local and staging environments. It won't break production (so you don't get surprise 500s from an edge case you missed), but it will loudly interrupt your local dev flow the moment you write N+1-prone code — which is exactly when you want to catch it.

Telescope, if you already have it installed, gives you the same visibility in a shared environment, which is useful when a teammate's code introduces the problem and it doesn't show up on your machine.

Fixing It: Beyond the Basic with()

The standard fix is eager loading:

$posts = Post::with('author')->get();

Now it's 2 queries total, regardless of whether you have 10 posts or 10,000. But real applications rarely stay this simple. Here are the patterns that actually come up.

Nested relationships

$posts = Post::with('author.company')->get();

This eager-loads the author, and the author's company, in one additional query per level — not one per post.

Loading only what you need

Pulling entire related models when you only need a couple of columns wastes memory and query time:

$posts = Post::with('author:id,name,email')->get();

Note: always include the foreign key (id here) or the relationship won't be able to match rows back together.

Conditional eager loading

Sometimes you only need the relationship in certain code paths. loadMissing avoids re-querying data that's already loaded:

$posts->each->loadMissing('author');

The one everyone forgets: counts

If you're displaying "12 comments" next to a post, don't load every comment just to count them:

// Bad: loads every comment into memory
$posts = Post::with('comments')->get();
foreach ($posts as $post) {
    echo $post->comments->count();
}

// Good: one query, no comment models loaded
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
    echo $post->comments_count;
}

The N+1 That Hides in API Resources

This is the one that catches even experienced developers. You eager load correctly in the controller:

$posts = Post::with('author')->paginate(20);
return PostResource::collection($posts);

But then inside PostResource, someone reaches one level deeper:

public function toArray($request): array
{
    return [
        'title' => $this->title,
        'author_name' => $this->author->name,
        'company' => $this->author->company->name, // <- not eager loaded!
    ];
}

The controller's with('author') never told Eloquent to load author.company. This relationship gets lazy-loaded once per post, and because it's buried inside the resource transformation layer, it's easy to miss in a quick code review. The fix is simply making sure your eager loading chain matches everything your resource actually touches:

$posts = Post::with('author.company')->paginate(20);

This is exactly the kind of bug preventLazyLoading in strict mode will catch immediately in your local environment, before it ever reaches a code reviewer.

A Quick Mental Model

Before shipping any endpoint or page that lists multiple models, ask: "For each row, am I accessing anything on a relationship?" If yes, that relationship needs to be in your with() call — including inside Blade views, API resources, and any Livewire component computed properties. The query count for a list page should not grow as the list grows. If it does, something in the chain is lazy loading.

Wrapping Up

N+1 queries aren't a sign of bad developers — they're a natural consequence of how readable and expressive Eloquent's relationship syntax is. The same $post->author->name that makes the code pleasant to write is what hides the extra query. The fix isn't to memorize every possible N+1 pattern; it's to make your tooling surface the problem automatically (preventLazyLoading in non-production environments) and to build the habit of tracing relationship access all the way through to your views and resources, not just your controllers.

Turn on strict mode this week. You'll probably find at least one N+1 you didn't know you had.

Have a project in mind?

Let’s discuss what you want to build.

Share your idea, current problem, or existing Laravel system. I will help identify the most practical next step.

Start a conversation

Continue reading

More Laravel guides

View all articles →
Birendra Jung Rai

Birendra Jung Rai

Laravel Engineer • System Architect • Technical Educator