Locking Down Accidental Lazy Loading With Model::preventLazyLoading()
Every Laravel developer has shipped an N+1 query at least once. You test a feature locally with three seed rows, everything feels instant, you ship it, and three weeks later someone with 4,000 real customers in the database is wondering why the reports page takes 12 seconds to load. The bug was there the whole time. Your test data just wasn't big enough to notice.
There's a one-line fix that catches this class of bug before it ever leaves your machine.
The setting
In your AppServiceProvider's boot() method:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
With this in place, the moment your code accesses a relationship that wasn't eager loaded, Eloquent throws a LazyLoadingViolationException instead of silently firing an extra query.
$posts = Post::all(); // no eager loading
foreach ($posts as $post) {
echo $post->author->name; // boom: LazyLoadingViolationException
}
That exception is annoying in exactly the way it should be. It forces you to fix the query right then, while you're still looking at the code, instead of finding out from a slow query log alert six months from now.
Why gate it on isProduction()
Notice the ! $this->app->isProduction() condition. You want this behavior loud and disruptive in local development and CI, where breaking things immediately is a feature. You almost certainly don't want it throwing in production, where an unexpected lazy load is a performance problem, not a reason to 500 a page for a real customer. Laravel gives you a graceful middle path for exactly this: log the violation instead of throwing.
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
Log::warning("Lazy loaded [{$relation}] on [{$model::class}]", [
'id' => $model->getKey(),
]);
});
Combine both and you get hard failures locally, where you can fix things immediately, and a paper trail in production for anything that slips through, without breaking a live page for a paying customer.
Where this pays off beyond the obvious
The most common place people first add this is a Blade view iterating a collection, but it catches a wider net than that. API Resources are a frequent offender, since it's easy to reference a relationship inside toArray() without realizing the controller never eager loaded it:
class PostResource extends JsonResource
{
public function toArray($request)
{
return [
'title' => $this->title,
'author' => $this->author->name, // caught if not eager loaded
];
}
}
It also catches lazy loading that sneaks in through conditionals, the kind where with('comments') was added months ago but a newer feature branch added a reference to $post->tags that nobody remembered to eager load.
What it won't catch
preventLazyLoading() only fires on relationship access, not on every possible inefficient query pattern. It won't warn you about a missing index, a SELECT * pulling columns you don't need, or a query that's technically eager loaded but loading way more data than the view actually uses. Pair it with Laravel Telescope or Debugbar in local development if you want visibility into the full query count and timing, preventLazyLoading() is specifically about the relationship-access blind spot, not a general query profiler.
Turning it on for the first time
If you're adding this to an existing app rather than a fresh one, expect some noise the first time you flip it on. Run your test suite with it enabled before you touch a real browser, you'll likely surface a handful of lazy loads that have been quietly running in the background for a while. Fix them as they come up, and from that point forward the exception does the N+1 hunting for you, for free, every single time you write a new feature.