Understanding and Solving the N+1 Problem in Laravel
Learn about the N+1 problem, why it degrades application performance, and how to detect and resolve it in Laravel using eager loading.
One of the most persistent performance bottlenecks in database-driven applications is the N+1 query problem. While ORMs like Laravel's Eloquent make working with relational databases exceptionally productive, their lazy-loading capabilities can quietly generate hundreds of redundant database queries if left unchecked.
In this dispatch, we will examine the mechanics of the N+1 problem, examine detection strategies, and review best practices for eliminating it via eager loading.
What is the N+1 Problem?
The N+1 problem arises when an application executes one primary query to retrieve a parent dataset of $N$ records, and then sequentially fires $N$ additional queries to retrieve the related data for each individual parent record.
Consider a practical scenario: displaying 25 blog articles along with the author's name:
- The initial query retrieves 25 post records:
1 query - Iterating through each post queries the author by ID:
25 queries - Total queries dispatched to the database: 26 queries
While 26 queries might execute quickly on a local development machine with low latency, in production under high concurrency, this multiplies into thousands of redundant database round-trips that degrade database connection pools and response times.
A Typical Laravel Example
Consider a Post model with a belongsTo relationship to a User:
Code • php// app/Models/Post.php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Post extends Model { public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } }
Now observe how standard controller or Blade view code causes the N+1 trap:
Code • php// Controller or Route $posts = Post::all(); // 1 query: SELECT * FROM posts foreach ($posts as $post) { // Fired on each iteration: SELECT * FROM users WHERE id = ? echo $post->author->name; }
Because $post->author is accessed lazily on each iteration, Eloquent issues a new SQL query for every single post in the collection.
How to Detect the N+1 Problem
Detecting N+1 queries during local testing is far easier than diagnosing them in production under load. Here are three effective techniques:
1. Model::preventLazyLoading (Strict Mode)
Modern versions of Laravel include built-in guards against lazy loading. You can enforce this in app/Providers/AppServiceProvider.php:
Code • phpnamespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { // Throws a LazyLoadingViolationException in local development Model::preventLazyLoading(! $this->app->isProduction()); } }
When enabled, any un-eager-loaded relationship access immediately halts execution with a detailed stack trace in development, making it impossible for an N+1 query to slip into production.
2. Laravel Debugbar
The popular Barryvdh Laravel Debugbar extension visualizes every database query executed on a request. Identical queries grouped together with different parameter bindings clearly signal an unoptimized relationship loop.
3. Database Query Listening in Logs
You can also log SQL statements to your development log:
Code • phpuse Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; public function boot(): void { if ($this->app->environment('local')) { DB::listen(function ($query) { Log::channel('daily')->debug($query->sql, [ 'bindings' => $query->bindings, 'time_ms' => $query->time, ]); }); } }
The Solution: Eager Loading
The remedy for the N+1 problem is Eager Loading, which informs Eloquent to preload related entities using a minimal number of optimized queries.
Using with()
Pass relationship names to the with method when initiating the query:
Code • php// 2 total queries executed regardless of post count: $posts = Post::with('author')->get(); foreach ($posts as $post) { echo $post->author->name; // Relies on in-memory mapping; 0 extra queries! }
Behind the Scenes: The SQL Breakdown
Under the hood, Eloquent executes exactly two queries:
Code • sql-- Query 1: Fetch the primary posts SELECT * FROM `posts`; -- Query 2: Fetch all authors associated with the retrieved post IDs in a single pass SELECT * FROM `users` WHERE `id` IN (1, 4, 9, 12, 18);
Eloquent then hydrates the parent models and establishes the relation references in memory. The database workload drops from $N + 1$ queries down to 2 queries.
Nested Eager Loading
When models have multi-level relationships (e.g., posts with authors and author profiles):
Code • php$posts = Post::with(['author.profile', 'comments.author'])->get();
Lazy Eager Loading with load()
If a model collection is already instantiated, use load() to eagerly fetch relationships conditionally:
Code • php$posts = Post::all(); if ($request->boolean('include_authors')) { $posts->load('author'); }
Conclusion
The N+1 problem is an avoidable tax on database infrastructure. By establishing automated guardrails such as Model::preventLazyLoading() in development and adopting proactive eager loading with with() across your query repositories, your Laravel applications will remain performant, predictable, and scalable under heavy traffic.
Ersin Demirtas
Software Engineer based in London, UK. Building web applications, APIs, and tools with Laravel, TypeScript, React, and Next.js.
Related Articles
Laravel and Me: A Senior Software Engineer’s Perspective
Feb 4, 2025 —I have been using Laravel since version 4.1 and have stuck with it for over a decade. Here is an honest appraisal of why Laravel continues to anchor my development work.
What Are the 4 Object-Oriented Principles?
Apr 11, 2025 —A clear exploration of the four foundational object-oriented programming principles and how they directly apply to modern Laravel applications.