[ GUIDE ]

How to Detect N+1 Queries in Laravel Production

Four concrete steps — from reproducing it locally with preventLazyLoading to catching it across real traffic.

QUICK ANSWER

How do I detect N+1 queries in Laravel production?

Call Model::preventLazyLoading() in AppServiceProvider to catch them in development, use eager loading (with(), load()) to fix them, and install a production APM that records query traces. NightOwl shows the full query list per request and per-pattern call counts on the Queries page — when you see the same fingerprint repeating inside one request, that's an N+1.

Updated · 2026-04-13

1. Reproduce it locally

The fastest way to spot an N+1 is to count queries per request. Laravel Telescope (official, free) or laravel-debugbar both expose this.

bash
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate

Load the slow page, then open /telescope/queries. If one endpoint fires 50+ similar queries, you have an N+1.

2. Fail loud in development

Eloquent lets you throw an exception whenever a relationship is lazy-loaded. This surfaces N+1s at the moment they're introduced, not weeks later when the page is slow.

app/Providers/AppServiceProvider.phpphp
<?php
namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Strict in dev and staging. Silent in production so we don't break
        // real users — we'll monitor N+1s in prod with an APM instead.
        Model::preventLazyLoading(! app()->isProduction());
    }
}

Now lazy loading a relation in development throws LazyLoadingViolationException with the exact relationship name. Your tests will catch it too.

3. Fix it with eager loading

The idiomatic fix is with() at query time or load() after the fact.

Before — fires 51 queries for 50 posts

php
$posts = Post::latest()->take(50)->get();

foreach ($posts as $post) {
    echo $post->author->name; // 1 query each — 50 extra queries
}

After — fires 2 queries

php
$posts = Post::with('author')->latest()->take(50)->get();

foreach ($posts as $post) {
    echo $post->author->name; // in-memory, no query
}

For nested relationships, use dotted syntax. For polymorphic relations, use morphWith(). For column-selected eager loads:

php
Post::with([
    'author:id,name,avatar',
    'comments' => fn($q) => $q->latest()->limit(3),
    'tags',
])->paginate(25);

4. Catch them in production

preventLazyLoading only helps in environments you actively test. In production you need an APM that observes real requests and flags N+1 patterns across traffic.

Laravel Telescope is not production-grade — it writes inline to the request lifecycle and has no aggregation. Options that are:

  • Laravel Nightwatch Cloud — official, records every query with trace context
  • NightOwl — BYOD Postgres dashboard on the same Nightwatch package
  • Sentry Performance — generic APM, weaker at Laravel-specific patterns

THE EASY WAY

NightOwl gives you the data to spot N+1s in real traffic

NightOwl captures every SQL statement per request and groups them by fingerprint. The Queries page shows per-pattern call_count across all traffic; the request detail page shows the full ordered query list for a single request. To find an N+1: open a slow request, scan its query list for the same fingerprint repeating dozens of times — that's your culprit, with the controller + route attached.

NightOwl doesn't auto-flag N+1 patterns; you do the diagnosis from the data. The win is having the production data at all — without an APM, you're guessing from local dev or staging.

bash
composer require nightowl/agent
php artisan nightowl:install

Data lives in your PostgreSQL. From $5/month flat, 14-day free trial, no credit card.

Frequently asked questions

What is an N+1 query problem in Laravel?

An N+1 query happens when Laravel makes one query to fetch a list of parent records (the '1'), then one additional query per parent to fetch a related record (the 'N'). Loading 50 posts and then their authors one-by-one means 51 queries instead of 2. It's the single most common cause of slow Laravel endpoints.

How do I detect N+1 queries in Laravel locally?

Enable Laravel Telescope or Debugbar in development — both show queries per request. For a code-level fix-first approach, call Model::preventLazyLoading() in AppServiceProvider so lazy loads throw a LazyLoadingViolationException in non-production environments. In production, you need an APM that groups N+1 patterns across real traffic.

How do I fix an N+1 query?

Use eager loading with with() or load(): User::with('posts')->get() fires two queries (users + posts) regardless of user count. For nested relations, use dotted syntax: User::with('posts.comments'). For polymorphic relationships, use morphWith(). For conditional eager loading, use with(['relation' => fn($q) => $q->where(...)]).

Can I detect N+1 queries in production?

Yes, but not with Telescope (it's not production-grade). You need an APM that captures query traces per request and groups identical query patterns. NightOwl records every query against your app with its fingerprint and a per-pattern occurrence count on the Queries page — open a slow request, scan its query list for the same fingerprint repeating, and you've found your N+1.

Should I always eager load everything?

No — over-eager-loading is the opposite mistake. If you eager-load a relation you only use on 10% of records, you load data you don't need. Profile real traffic and eager-load based on actual access patterns, not defensive assumptions.

Does preventLazyLoading break production?

Only if you leave it on in production. The canonical pattern is Model::preventLazyLoading(! app()->isProduction()) — strict in dev and staging, silent in production. In production you detect N+1s via monitoring, not by throwing exceptions that would break user requests.

What's the performance impact of an N+1 query?

Each query has ~1-5ms of round-trip overhead on top of actual execution. A 100-item page with an N+1 adds 100-500ms of pure latency — often the difference between a 200ms page and a 2-second page. The problem scales linearly with list size.

PRICING

Flat pricing. No event caps. No per-seat fees.

14-day free trial, no credit card. Your PostgreSQL, your data.

HOBBY

$5 /month

1 app · 14 days lookback · all Laravel events

TEAM

$15 /month

Up to 3 connected apps · unlimited environments · all Laravel events

AGENCY

$69 /month

Unlimited apps · unlimited agent instances · same flat rate at any traffic

Related