You shipped the posts page with with('author'). Two sprints later someone adds the comment count to the Vue card, forgets the with(), and the debug bar shows 51 queries again. Laravel has a one-line switch that catches that for you, and most apps have never turned it on.
TL;DR
- Call
Model::automaticallyEagerLoadRelationships()inAppServiceProvider::boot(). When you touch a relation that is not loaded on one model, Laravel loads it for every model in the same collection with onewhere in (...)query. - It also works down the chain: users, then posts, then comments ran as 3 queries instead of 61 in our test.
- Prefer a per-query opt-in? Call
->withRelationshipAutoloading()on one Eloquent collection. - It is not a replacement for thinking: relation methods like
$post->comments()->count()and a fresh query inside a loop still run once per row.
What automatic eager loading is and why people miss it#
Automatic eager loading shipped in Laravel 12.8 (April 2025, PR #53655). Laravel 12.9 added withRelationshipAutoloading() on single models and fixed a closure serialization error with queued models. The Laravel 12 docs labelled the feature beta. The 13.x docs no longer carry that warning.
It sits between “Lazy Eager Loading” and “Preventing Lazy Loading” in a very long relationships page. If you learned Eloquent before 2025, you already “know” eager loading and never scroll back.
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::automaticallyEagerLoadRelationships();
}That is the whole setup. Every Eloquent collection a query returns now carries a small callback. The first time you read a missing relation on any model in it, Laravel runs loadMissing() for the whole collection instead of lazy loading one row.
The usual way vs the gem#
The classic fix is to list every relation the page reads in with(). It works, until the page and the with() list drift apart.
with() you have to remember, then automatic eager loading:
public function index(): Response
{
$posts = Post::query()
->with(['author', 'comments']) // must match what the page reads
->latest('id')
->paginate(10);
return Inertia::render('Posts/Index', [
'posts' => $posts->through(fn (Post $post) => [
'id' => $post->id,
'title' => $post->title,
'author' => $post->author->name,
'comments' => $post->comments->count(),
]),
]);
}public function index(): Response
{
$posts = Post::query()
->latest('id')
->paginate(10);
return Inertia::render('Posts/Index', [
'posts' => $posts->through(fn (Post $post) => [
'id' => $post->id,
'title' => $post->title,
'author' => $post->author->name,
'comments' => $post->comments->count(),
]),
]);
}We ran both in a scratch Laravel 13.37 app with 10 authors, 50 posts and 200 comments, counting queries with DB::listen:
| Code | Lazy (default) | with() | Automatic |
|---|---|---|---|
Post::all() then $post->author->name | 51 | 2 | 2 |
| Users → posts → comments (docs example) | 61 | 3 if you list both | 3 |
Same, plus $comment->author (4 levels) | 261 | 4 if you list all three | 4 |
paginate(10) + author + comments (Inertia page above) | 22 | 4 | 4 |
The automatic version produced the exact same SQL as a hand-written with(): select * from "authors" where "authors"."id" in (1, 2, …). It just decides at read time instead of query time.
Tip: Not ready to flip it globally?
Turn it on for one result set: Post::all()->withRelationshipAutoloading(). For a paginator, call it on the page’s collection: $posts->getCollection()->withRelationshipAutoloading(). Both gave us 2 and 3 queries with the global switch off.
Real-world example: an Inertia posts page#
The “automatic eager loading” controller above is the real one from the lab. Here is the Vue page that reads those props. Nothing changes on the front end, which is the point: the backend stops caring which fields the card grows next sprint.
<script setup lang="ts">
defineProps<{
posts: {
data: { id: number; title: string; author: string; comments: number }[]
}
}>()
</script>
<template>
<ul>
<li v-for="post in posts.data" :key="post.id">
{{ post.title }} · {{ post.author }} · {{ post.comments }} comments
</li>
</ul>
</template>We hit that route through the HTTP kernel with an X-Inertia header. The page ran four queries on the posts database: the paginator count, the page of posts, one authors query, one comments query.
API resources work the same way. A PostResource that reads $this->author->name, wrapped in PostResource::collection(Post::all()), ran 2 queries instead of 51.
It plays nicely with preventLazyLoading#
A lot of apps run Model::preventLazyLoading(! app()->isProduction()) (or Model::shouldBeStrict()) to make N+1 loud in development. Turn on automatic eager loading as well and the autoloader runs first:
| Settings | Post::all() + $post->author |
|---|---|
preventLazyLoading() only | LazyLoadingViolationException |
preventLazyLoading() + automatic | 2 queries, no exception |
That is good news for production and a trade-off for development. Your strict mode stops telling you where with() is missing, because nothing is missing any more. If you want the warnings for a while, keep strict mode alone in local and enable automatic loading only in production:
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
Model::automaticallyEagerLoadRelationships($this->app->isProduction());
}Gotchas#
Touch it once, load it for everyone. We read $posts->first()->author for a page heading. Laravel loaded the author for all 50 posts, because that is the whole idea. On a 5,000-row export where you only need one author, that is 4,999 rows you did not ask for.
Gotcha: Bigger Inertia payloads
A loaded relation is serialized. In the lab we passed a raw collection of posts as an Inertia prop after reading $posts->first()->author once. Every post in the JSON now carried a full author object. If you pass models straight to Inertia, map them to arrays or use an API resource so the prop only ships the fields you chose.
Relation methods are not covered. The autoloader lives on the dynamic property. $post->comments()->count() in a loop still ran 51 queries. $post->comments->count() ran 2, but it loads every comment just to count them. For counts, withCount('comments') is still the right tool: 1 query.
A new query per row is still N+1. Each query returns its own collection. Post::where('id', $id)->get() inside a foreach gets no help, because each collection only has one model in it.
Child to parent costs one extra query. Reading $comment->post while looping a post’s comments ran one more posts where id in (...) query, for posts you already had in memory. chaperone() on the hasMany hydrates the parent for free (2 queries instead of 3). The two features stack fine.
Single models gain nothing. Post::find(1)->author still runs 2 queries, the same as lazy loading. Post::create() followed by ->author lazy loads as usual. The win only appears when there is a collection to batch.
Limited relations stay correct. A hasMany(...)->latest('id')->limit(2) relation auto-loaded with a row_number() over (partition by ...) window query, so each post still got its own 2 latest comments, not 2 in total.
Factories switch it off while making models, so your seeders and tests behave the way they did before (PR #56211).
Try it yourself#
Count the queries in tinker
php artisan tinkeruse App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
User::all()->each(fn ($user) => $user->posts->count());
count(DB::getQueryLog()); // 1 + one per user
Model::automaticallyEagerLoadRelationships();
DB::flushQueryLog();
User::all()->each(fn ($user) => $user->posts->count());
count(DB::getQueryLog()); // 2Swap User and posts for any model and hasMany you already have.
How useful is this?#
| Score | |
|---|---|
| Time saved per use | ★★★★★ One line in a service provider, no hunting with() lists |
| How often you’ll reach for it | ★★★★★ Every list page, resource collection and export |
| Risk if misused | ★★☆☆☆ Extra rows loaded and bigger JSON if you pass raw models |
| Overall | 4.5 / 5: the cheapest N+1 insurance Laravel ships |
If your app has a list page and a debug bar, try it today and compare the query count.
Recap#
Model::automaticallyEagerLoadRelationships()makes a missing relation load once for the whole collection, at any depth.->withRelationshipAutoloading()gives you the same thing for one collection or model.- It runs before
preventLazyLoading(), so strict mode stops throwing for collection models. - Relation methods, per-row queries and single models get no help. Use
withCount, batch your queries, andchaperone()for child-to-parent. - Shipped in Laravel 12.8; tested here on Laravel 13.37.
FAQ#
Does automatic eager loading replace with()?
It makes a forgotten with() cheap, and it runs the same SQL. Keep explicit with() where you want the query shape visible in the controller, or where you constrain the eager load with a closure.
Which Laravel version do I need?
Laravel 12.8 or newer for Model::automaticallyEagerLoadRelationships(), and 12.9 or newer for withRelationshipAutoloading() on a single model. We tested on Laravel 13.37.
Will it break preventLazyLoading or shouldBeStrict?
Nothing breaks, but the autoloader runs before the lazy loading check, so collection models stop throwing LazyLoadingViolationException. Keep strict mode alone in development if you still want those warnings.
Is it safe for Inertia props?
Yes, as long as you map models to arrays or API resources. A raw model collection serializes every relation that got loaded, including ones you only touched once.
Does it work with paginate()?
Yes. The page of results is an Eloquent collection, so paginate(10) plus author and comments ran 4 queries: count, page, authors, comments.
Next steps#
- Eager load a relation for a Vue list, the manual
with()version - API resources for Vue, to keep Inertia payloads to the fields you chose
- Inertia partial reloads, so heavy props are not reloaded on every visit
- Laravel docs: Automatic eager loading
Sources#
- Laravel 13 docs: Automatic eager loading
- Laravel 13 docs: Preventing lazy loading
- Laravel 13 docs: Automatically hydrating parent models (
chaperone) - laravel/framework v12.8.0 release (PR #53655)
- laravel/framework v12.9.0 release (PRs #55344, #55345)
- laravel/framework PR #56211: factories and automatic eager loading
- Source:
Illuminate\Database\Eloquent\Collection::withRelationshipAutoloading



