This is the single most useful database habit to get right in Laravel, because the wrong choice is the N+1 query, and the N+1 is the most common reason a Laravel page is slow. The mechanics are simple once you've seen them, and the fix is usually one method call. The hard part is noticing you needed it.
Lazy loading, by default
Eloquent loads relationships on demand. Accessing $user->posts fires a query at the moment of access:
$users = User::take(100)->get(); // 1 query
foreach ($users as $user) {
echo $user->posts->count(); // 1 query per user — 100 total
}
// Total: 1 + 100 = 101 queries → N+1Eager loading with with()
$users = User::with('posts')->take(100)->get(); // 2 queries total
foreach ($users as $user) {
echo $user->posts->count(); // no query fires — already loaded
}
// Total: 2 queries, regardless of user count
Under the hood: one SELECT * FROM users LIMIT 100, then one SELECT * FROM posts WHERE user_id IN (1, 2, 3, ..., 100). Eloquent wires the posts back to their users in-memory.
Nested relationships
// Load users + their posts + each post's comments + each comment's author
User::with('posts.comments.author')->get();
// 4 queries total:
// 1. SELECT * FROM users
// 2. SELECT * FROM posts WHERE user_id IN (...)
// 3. SELECT * FROM comments WHERE post_id IN (...)
// 4. SELECT * FROM users WHERE id IN (...) -- comment authorsFour queries for any number of rows. Beats N+1-squared-squared-squared, which would be thousands.
Constraining eager loads
// Only eager-load published posts
User::with(['posts' => fn ($q) => $q->where('published', true)])->get();
// Only specific columns (reduces memory)
User::with('posts:id,user_id,title')->get();
// Load counts without the rows themselves
User::withCount('posts')->get();preventLazyLoading — catch N+1 in dev
app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
In non-production, any lazy-loaded relationship throws LazyLoadingViolationException. You're forced to either eager-load or explicitly $user->loadMissing('posts'). Catches N+1 as a bug during development rather than as a slow page in production.
Don't enable in production — a missed eager-load becomes a 500 for real users instead of a slow page.
When the models are already in hand: load()
with() works when you control the query.
Sometimes you don't — a collection gets passed into a method, or you fetched it
before you knew you'd need the relationship. load()
is the after-the-fact version:
$users = User::take(100)->get(); // already fetched, no relations
// ...later, once you know you need them:
$users->load('posts'); // one batched query, not 100
// inside reusable code, prefer loadMissing — it skips models
// that already have the relation loaded:
$users->loadMissing('posts');
Rule of thumb: with() when you're writing
the query, load() or
loadMissing() when the models already
exist.
When lazy loading is actually fine
On a single model, not a collection: $user = User::find(1); $user->posts; fires one query. No N+1. The problem only materializes with loops over collections.