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.
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.
<?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
$posts = Post::latest()->take(50)->get();
foreach ($posts as $post) {
echo $post->author->name; // 1 query each — 50 extra queries
}After — fires 2 queries
$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:
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.
composer require nightowl/agent
php artisan nightowl:installData lives in your PostgreSQL. From $5/month flat, 14-day free trial, no credit card.