
August 12, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Slow page loads in Laravel applications often trace back to a single architectural oversight: the N+1 query problem. When your code iterates over a collection and accesses a relationship inside the loop, Eloquent executes one initial query plus one additional query for every record returned. For a list of 50 items, that is 51 database round-trips instead of two. Effective Laravel N+1 query detection and fixes require moving beyond basic eager loading to include strict development configurations, automated testing constraints, and runtime profiling. If you are building data-intensive platforms, understanding these patterns is as critical as knowing Laravel API best practices or proper indexing strategies.
with() method to eager load relationships upfront, and verifying improvements via Laravel Debugbar or Telescope. This reduces hundreds of redundant queries to just two optimized statements.What causes the N+1 problem in Laravel Eloquent?
The N+1 problem occurs when an application fetches a parent model and then lazily loads related models within a loop. Eloquent’s lazy loading feature is convenient for single-record views but catastrophic for collections. In my experience maintaining legal-tech portals and eCommerce systems, this is the most frequent cause of performance degradation as datasets grow from dozens to thousands of records.
Consider a blog listing page. Without eager loading, fetching 20 posts triggers one query for the posts table. As Blade iterates through each post to display $post->author->name, Eloquent fires a separate SELECT for each author. With 20 posts, you execute 21 queries. Scale that to a directory site like Lawyers Pokhara displaying 100 lawyers with their firm details, and you are executing 101 queries per page load. This latency compounds quickly, especially on shared hosting or budget VPS infrastructure common in Nepal where database I/O is often the primary bottleneck.
Why lazy loading exists
Eloquent defaults to lazy loading because it simplifies code for single-model contexts. When viewing a specific case detail on a legal portal, loading the client relationship on-demand is perfectly efficient. The problem arises only when developers apply single-record patterns to collection contexts without realizing the multiplicative cost. Understanding this distinction is fundamental to writing performant PHP applications.
How do you detect N+1 queries in Laravel 12?
Detection must happen before code reaches production. Relying on users to report slow pages is unacceptable for professional Laravel development services. Laravel 12 provides multiple layers of defense, from local development guards to CI pipeline enforcement.
Enable strict mode in development
Laravel’s strict mode prevents lazy loading entirely outside of production. Add this to your AppServiceProvider:
<?php namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { if ($this->app->environment('local', 'testing')) { Model::preventLazyLoading(); Model::preventAccessingMissingAttributes(); } } }With preventLazyLoading() active, any attempt to access an unloaded relationship throws a LazyLoadingViolationException. This forces you to address the issue immediately during development rather than discovering it months later when traffic increases. On projects I maintain, this guard has caught dozens of potential N+1 issues before they ever reached staging.
Use Laravel Debugbar for query counting
The Laravel Debugbar package displays real-time query counts, execution time, and duplicate detection in your browser toolbar. Install it as a dev dependency:
composer require barryvdh/laravel-debugbar --devNavigate to the problematic page and check the "Queries" tab. Sort by execution time or look for repeated identical queries — a hallmark of N+1 problems. Debugbar also highlights queries lacking indexes, which often compound N+1 latency. For deeper inspection in API-heavy applications, Laravel Telescope provides similar insights with request-level tracing.
Log slow queries in production
In production, you cannot enable strict mode without risking outages. Instead, configure MySQL or PostgreSQL to log queries exceeding a threshold. For MySQL 8.4:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';Review these logs weekly. Queries appearing repeatedly with similar patterns usually indicate unresolved N+1 issues or missing indexes. Pair this with application-level monitoring like Sentry or Grafana to correlate database load with specific routes and controllers.
How do you fix N+1 queries with eager loading?
Eager loading is the primary solution. It instructs Eloquent to fetch all related records in a single batched query using WHERE IN clauses. The implementation varies based on relationship complexity and whether you need all columns.
Basic eager loading with with()
The simplest fix replaces lazy access with explicit preloading:
// Before: N+1 problem $posts = Post::all(); // After: Fixed with eager loading $posts = Post::with('author')->get();This executes exactly two queries regardless of collection size. For nested relationships, chain them with dot notation:
$lawyers = Lawyer::with(['firm.address', 'specializations'])->get();This loads lawyers, their firms, each firm’s address, and all specializations in four total queries instead of potentially hundreds.
Constraining eager loaded queries
Sometimes you need only specific related records or columns. Use closure-based constraints to avoid over-fetching:
$orders = Order::with(['items' => function ($query) { $query->where('status', 'shipped') ->select('id', 'order_id', 'product_name', 'quantity'); }])->get();Always include the foreign key (order_id) in constrained selects — Eloquent needs it to match children to parents. Omitting it silently returns empty relationships, a bug I have debugged more than once on client projects.
Eager loading on existing collections
If you already have a collection and realize relationships are missing, use load() or loadMissing() instead of re-querying:
$posts = Post::all(); // Later in the same request... $posts->loadMissing(['author', 'comments.user']);loadMissing() is preferable because it skips relationships already loaded, preventing redundant queries when called conditionally. This pattern is useful in service classes or view composers where the full relationship graph isn’t known at initial fetch time.
When should you avoid eager loading in Laravel?
Eager loading is not universally optimal. Misapplying it can waste memory, increase payload size, or create worse performance than the original N+1. Understanding trade-offs separates senior engineers from those who mechanically apply patterns.
Large result sets with selective access
If you fetch 1,000 orders but only display the first 10 with items, eager loading all 1,000 item relationships wastes significant memory and bandwidth. Paginate first, then eager load only the visible page:
$orders = Order::with('items') ->orderByDesc('created_at') ->paginate(10);For APIs returning large datasets where consumers may not need all relationships, consider making eager loading optional via query parameters or using API resources to conditionally include relationships based on consumer needs.
Polymorphic relationships with many types
Eager loading polymorphic relations (e.g., commentable spanning Posts, Videos, Articles) generates one WHERE IN query per morph type. If comments reference 10 different entity types, you still execute 11 queries. In such cases, denormalizing frequently accessed fields (like storing commentable_title directly on the comments table) may outperform eager loading. Evaluate based on actual query logs, not assumptions.
Deeply nested chains with low cardinality
Loading users.posts.comments.tags.authors across five levels can produce unexpectedly large intermediate result sets even if final output is small. Profile before optimizing. Sometimes breaking into two separate queries with manual hydration in PHP is faster than forcing Eloquent to join massive intermediate sets. Always measure with realistic production-scale data, not seed factories generating uniform distributions.
| Scenario | Recommended Approach | Why |
|---|---|---|
| List page with consistent relations | with() eager loading | Predictable 2-query pattern, minimal overhead |
| API with optional includes | Conditional when() eager loading | Avoids fetching unused data for lightweight consumers |
| Single record detail view | Lazy loading acceptable | One extra query is negligible vs. premature optimization |
| Polymorphic with 5+ types | Denormalize or accept N+1 | Eager load generates N type-specific queries anyway |
| Report aggregation | Raw query or subquery | Eloquent hydration overhead exceeds benefit |
How do you prevent N+1 regressions in CI pipelines?
Fixing existing N+1 issues is reactive. Preventing regressions requires automated enforcement integrated into your deployment workflow. On projects deployed via Deployer 7 and GitLab CI, I treat query violations as test failures.
Fail tests on lazy loading violations
Since strict mode is enabled in the testing environment, any test triggering lazy loading fails automatically. Write feature tests that exercise collection endpoints:
public function test_lawyer_index_does_not_trigger_lazy_loading(): void { Lawyer::factory()->count(20)->create(); // Throws LazyLoadingViolationException if N+1 exists $response = $this->getJson('/api/lawyers'); $response->assertOk() ->assertJsonCount(20, 'data'); }This catches regressions introduced by refactoring, new features, or package upgrades. Run these tests in every merge request pipeline. A failing test blocks deployment until the eager loading is restored or intentionally justified with a comment explaining why lazy loading is acceptable in that specific context.
Assert query counts in tests
Beyond strict mode, explicitly assert expected query counts to catch subtle regressions:
public function test_order_list_executes_exactly_two_queries(): void { DB::enableQueryLog(); Order::with('customer')->take(50)->get(); $queries = DB::getQueryLog(); $this->assertCount(2, $queries); }This documents performance expectations alongside behavior. When someone adds a new column access that triggers lazy loading, the assertion fails with a clear message. Combine this with database refreshes between tests to ensure consistent baseline state.
Monitor production query metrics
Even with perfect tests, production data distributions differ. Instrument your application to track query counts per route. Packages like spatie/laravel-query-builder or custom middleware can log metrics to Redis or Prometheus. Set alerts for routes exceeding baseline thresholds. This catches issues that only manifest with production-scale data volumes or specific user permission combinations that tests don’t cover. For teams managing database-driven websites, this monitoring is non-negotiable.
Implementing sustainable Laravel N+1 query detection and fixes
Eliminating N+1 queries is not a one-time cleanup but an ongoing engineering discipline. Enable strict mode locally and in tests today. Audit your highest-traffic routes with Debugbar this week. Add query count assertions to critical feature tests before your next release. These steps transform Laravel N+1 query detection and fixes from reactive firefighting into predictable, maintainable performance characteristics. If your team lacks bandwidth to systematically address accumulated technical debt, consider engaging experienced web developers in Nepal who understand both Eloquent internals and production infrastructure constraints. Performance work pays compounding returns — every millisecond saved at the database layer improves user experience, SEO rankings, and server costs simultaneously.

