Your ORM Wrote 300 Queries for One Page: Fixing N+1 in Laravel cover image

Your ORM Wrote 300 Queries for One Page: Fixing N+1 in Laravel

Scott Keck-Warren • October 16, 2026

Eloquent lets us list podcast episodes alongside their podcast names in just five lines:

$episodes = PodcastEpisode::all();
foreach ($episodes as $episode) {
    echo $episode->title, "<br>";
    echo $episode->podcast->name, "<br>";
}

With 20 rows, the page loads fast. In production, the same page runs more than 300 queries: one for the episodes and one for every $episode->podcast lookup. Nothing in the code hints at it, which is why the N+1 query problem shows up in so many Laravel apps.

Below I cover how to spot N+1 queries with three free tools, fix them with eager loading, and use Model::preventLazyLoading() so they fail in CI before they reach production.

What Is the N+1 Query Problem?

The loop above runs one query to fetch the episodes, then one more each time $episode->podcast reaches for a podcast that hasn't loaded yet:

select * from "podcast_episodes";
select * from "podcasts" where "podcasts"."id" = 2 limit 1;
select * from "podcasts" where "podcasts"."id" = 2 limit 1;
select * from "podcasts" where "podcasts"."id" = 2 limit 1;
-- ...one more for every episode

This is the "N+1 problem": 1 query for the original collection and N more for each row's relationship.

Eloquent (and other Active Record ORMs) loads relationships lazily by default. Lazy loading means the ORM fetches a relationship the first time code accesses it. Most of the time that's the right behavior, because we only want to load the relationships a page uses.

The catch is that test data can hide performance issues. Only once you push to production with 10,000 rows will they show up.

How to Detect N+1 Queries in Laravel

You can't fix what you can't see, and a slow page rarely tells you why. Laravel gives us three free ways to see every query a page runs:

Tool Setup Where to use it Best for
DB::listen() query log A few lines in AppServiceProvider Any environment Quick checks, queue jobs, CLI commands
Laravel Debugbar Composer dev dependency Local only Spotting duplicates while you click around
Laravel Telescope Composer package + migration Local and staging Reviewing requests nobody was watching

Log Queries with DB::listen()

DB::listen() hands us every query Laravel runs. A few lines in AppServiceProvider write them to the log:

// in AppServiceProvider::boot()
DB::listen(static function (QueryExecuted $query): void {
    Log::info("Query executed", [
        "sql" => $query->sql,
        "bindings" => $query->bindings,
        "time_ms" => $query->time,
    ]);
});

The N+1 shows up as the same query repeated over and over:

[2026-10-16 16:20:21] local.INFO: Query executed {"sql":"select * from \"podcasts\" where \"podcasts\".\"id\" = ? limit 1","bindings":[2],"time_ms":0.41}
[2026-10-16 16:20:21] local.INFO: Query executed {"sql":"select * from \"podcasts\" where \"podcasts\".\"id\" = ? limit 1","bindings":[2],"time_ms":0.38}
[2026-10-16 16:20:21] local.INFO: Query executed {"sql":"select * from \"podcasts\" where \"podcasts\".\"id\" = ? limit 1","bindings":[2],"time_ms":0.40}

It needs zero dependencies and works in any environment.

Laravel Debugbar

Laravel Debugbar is one of my favorite ways to find N+1 problems because it shows them inside the browser as you navigate your local environment. Run it locally because it exposes your queries, routes, and configuration to anyone who loads the page.

Laravel Debugbar Queries tab showing 307 statements executed with 300 duplicates, a long run of identical select queries against the podcasts table highlighted
Debugbar flags 300 duplicate queries on a single page load, one per episode asking for the same podcast.

Laravel Telescope

Laravel Telescope records every query to a database table, which makes it the best fit for staging, where nobody is watching a toolbar.

Laravel Telescope Queries screen listing the same select from podcasts where id = 2 query over and over, each taking under half a millisecond
Telescope records every query to the database, so the repeated podcast lookups show up on staging where nobody is watching a toolbar.

I prefer Laravel Debugbar because it gives me short feedback loops.

How to Fix N+1 Queries with Eager Loading

Eloquent's with() method "eager loads" a relationship: it fetches the related models for the whole collection in one query, up front.

$episodes = PodcastEpisode::with("podcast")->get();
foreach ($episodes as $episode) {
    echo $episode->title, "<br>";
    echo $episode->podcast->name, "<br>";
}

Eloquent now runs two queries no matter how many episodes we load:

select * from "podcast_episodes";
select * from "podcasts" where "podcasts"."id" in (2, 3);

with() also handles nested relationships with dot notation:

$episodes = PodcastEpisode::with("podcast.owner")->get();

When a page only needs a count, withCount() adds a {relation}_count attribute without loading the related models:

$episodes = PodcastEpisode::withCount("people")->get();
foreach ($episodes as $episode) {
    echo $episode->title, "<br>";
    echo "Guests: {$episode->people_count}<br>";
}

Prevent N+1 Queries with preventLazyLoading()

Enough people have been bitten by this that the Laravel team added Model::preventLazyLoading() to stop the problem before it reaches production.

// in AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());

Passing ! app()->isProduction() keeps the check off in production, so the page still loads if one slips through. It throws everywhere else, with local development and CI/CD being the most important.

Run the original loop, and Laravel stops it:

Attempted to lazy load [podcast] on model [App\Models\PodcastEpisode] but lazy loading is disabled.

When Eager Loading Is the Wrong Answer

You can also overdo eager loading.

This query loads every relationship on the model:

$episodes = PodcastEpisode::with([
    "podcast",
    "people",
    "guests",
    "partners",
    "files",
    "tasks",
    "shareables",
    "shortFormVideos",
    "transcripts",
    "keywordBoosts",
])->get();

That only helps if the page uses all ten relationships. Each eager load runs its own query, and every unused one wastes memory and CPU cycles.

Catch N+1 Queries Before Production

A checklist for keeping N+1 queries out of production:

  1. Run Laravel Debugbar locally and watch the duplicate count on every page you touch.
  2. Turn on Model::preventLazyLoading() outside production so a lazy load fails in CI.
  3. Use with() for relationships a loop reads and withCount() when you only need a count.
  4. Eager load only the relationships the page uses.
  5. Keep Telescope or a DB::listen() log on staging for pages nobody clicks through by hand.

N+1 queries are one slice of database performance. When a single query is slow on its own, EXPLAIN shows why. What are your favorite ways to catch slow queries?

AI notice:

The outline for this post was generated via AI, then I wrote the prose and ran it through Grammarly