Laravel Magazine
Locking Down Accidental Lazy Loading With Model::preventLazyLoading()

Locking Down Accidental Lazy Loading With Model::preventLazyLoading()

Eric Van Johnson ·

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.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.