Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel N+1 Query Detection and Fixes

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.

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.

Lazy Loading (N+1)SELECT * FROM postsSELECT * FROM authorsSELECT * FROM authorsSELECT * FROM authors1 + N Queries(Repeated DB Round Trips)Eager Loading (Fixed)SELECT * FROM postsSELECT * FROM authors WHERE id IN (...)Exactly 2 Queries(Batched Relationship Fetch)
Visual comparison of Laravel N+1 lazy loading versus eager loading query execution patterns

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 --dev

Navigate 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.

Need Related Data?Collection or Single?CollectionSingleUse with() / load()Lazy OK (usually)Need Constraints?YesNoClosure SelectSimple with()
Decision tree for selecting appropriate Laravel eager loading approach based on context

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.

ScenarioRecommended ApproachWhy
List page with consistent relationswith() eager loadingPredictable 2-query pattern, minimal overhead
API with optional includesConditional when() eager loadingAvoids fetching unused data for lightweight consumers
Single record detail viewLazy loading acceptableOne extra query is negligible vs. premature optimization
Polymorphic with 5+ typesDenormalize or accept N+1Eager load generates N type-specific queries anyway
Report aggregationRaw query or subqueryEloquent 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.

Git PushUnit TestsStrict Mode ONFail on Lazy LoadFeature TestsAssert Query Count≤ Expected ThresholdDeploy StagingProductionPipeline Blocks on Any Violation
CI pipeline integration points for automated Laravel N+1 query detection and prevention

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.

Frequently Asked Questions

An N+1 query occurs when code executes one initial database query followed by N additional queries inside a loop, typically caused by accessing Eloquent relationships without eager loading.

Install the laravel-debugbar package or use the preventNPlusOneQueries method in your AppServiceProvider boot method to throw exceptions immediately when lazy loading is detected outside of intended contexts.

Not always; eager loading large datasets can consume more memory than individual queries, so profile with Debugbar or Telescope before blindly adding with() clauses to every relationship.

Use dot notation within the with method, such as Post::with('comments.author')->get(), which loads posts, their comments, and each comment's author in exactly three queries regardless of result count. This pattern works identically in Laravel 11 and 12 on PHP 8.2 or higher. Always verify the generated SQL using Debugbar or query logging to confirm the nested eager load executed as expected rather than falling back to lazy loading silently.

Calling Model::preventLazyLoading() in your AppServiceProvider throws a LazyLoadingViolationException whenever a relationship is accessed without prior eager loading. In my experience working on production Laravel applications, enabling this only in non-production environments catches issues early without risking user-facing errors. You can allow specific models or relationships via allowLazyLoading() if certain legacy code paths cannot be refactored immediately. This safety net has prevented numerous N+1 regressions during feature development on legal-tech portals where document relationships are deeply nested and easily overlooked during rapid iteration cycles.

Use loadMissing when you are uncertain whether a relationship was already eager loaded earlier in the request lifecycle, such as in service classes or view composers that receive models from multiple sources. Unlike load, which always re-queries the database, loadMissing checks the relationLoaded state first and only queries if necessary. I have found this particularly useful in shared Blade components across projects like Ajako Deal where vendor listings may arrive pre-loaded from some controllers but not others. It prevents duplicate queries while maintaining flexibility in how models are passed through the application layers.

Yes, if you apply constraints that filter out parent records or use closures incorrectly, you may inadvertently trigger lazy loading for excluded relationships. For example, constraining a with clause to only load approved comments means accessing unapproved comments later will fire additional queries. Always test constrained eager loads with Debugbar open and verify the exact query count matches expectations. On eCommerce projects like Nepal Gift Card, I have seen order item relationships with status filters cause hidden N+1s when admin views needed all items regardless of approval state. Document your constraints clearly so future developers understand the loading boundaries.

Eager load all required relationships in the controller before passing data to API resources, and use the with property in JsonResource to declare dependencies explicitly. This ensures serialization never triggers lazy loading even when resources are nested. In REST APIs I have built for legal service platforms, failing to declare resource dependencies caused mobile apps to receive incomplete data or trigger hundreds of hidden queries during list endpoints. Always profile API endpoints separately from web routes because resource serialization happens after the controller returns, making N+1s harder to spot without dedicated API debugging tools or response logging middleware.

Eager loading hydrates all related models into memory at once, which becomes problematic when loading thousands of parent records with many children each. On a client project processing bulk exports, switching from eager loading to chunked lazy loading reduced peak memory from 800MB to under 100MB. Profile memory with xdebug or Blackfire before committing to eager loading on large datasets. Consider cursor pagination combined with selective column selection via select clauses to reduce hydration overhead. Sometimes running separate optimized queries and assembling results manually outperforms Eloquent eager loading for reporting workloads where object graph integrity matters less than raw throughput.

Polymorphic relationships require explicit eager loading using morphTo or morphMany with the specific types listed, otherwise Laravel queries the type table for each parent record individually. Debugbar shows these as repeated queries against the morphable type column. In my experience building multi-entity directories like Lawyers Pokhara, forgetting to specify morph types in with clauses caused severe N+1s when listing mixed professional profiles. Always declare all possible morph types upfront even if some are rarely used, because partial eager loading still triggers lazy queries for unspecified types. Test with diverse dataset samples to ensure all polymorphic variants are covered in your eager load strategy.

Proper indexes on foreign keys and morphable columns reduce the cost of both eager load queries and any remaining lazy loads, but they do not eliminate N+1s themselves. After fixing eager loading, add composite indexes matching your constraint patterns to speed up the consolidated queries. On WooCommerce integrations like Petals Nepal, adding indexes on post_type and meta_key columns reduced eager load query time by 60% after N+1 fixes were applied. Use EXPLAIN ANALYZE on your eager load queries to verify index usage. Remember that over-indexing slows writes, so balance read optimization against your application's write frequency and maintenance window tolerance.

Jobs often process collections serially, making them prime candidates for N+1s that go undetected because no HTTP response is measured. Always eager load relationships before dispatching jobs or within the job handle method before looping. Serialize only necessary model IDs rather than full models to avoid stale relationship states. In batch processing systems I have maintained for travel booking platforms, adding eager loading to export jobs reduced execution time from 45 minutes to under 8 minutes. Monitor job duration metrics alongside query counts because background N+1s accumulate silently and only surface during scaling events or timeout failures.

Yes, when displaying paginated lists where users rarely expand details, lazy loading individual records on demand can outperform eager loading entire relationship trees that mostly go unused. Conditional eager loading based on request parameters or user permissions avoids wasting queries on inaccessible data. On legal information sites like Court Marriage In Nepal, we lazy-load document attachments because most visitors only read summaries. The key is intentionality: document why lazy loading is chosen, monitor actual access patterns with telemetry, and set strict mode exceptions explicitly rather than leaving it as accidental default behavior that degrades over time.

Write integration tests using assertQueryCount or DB::getQueryLog assertions to enforce maximum query thresholds for critical endpoints. Combine this with factory-generated datasets that match production cardinality, because small test databases hide N+1s that appear at scale. In my testing workflow for Laravel 12 applications, I set baseline query counts after fixing known issues and fail tests exceeding 10% variance to catch regressions. Run these tests in CI pipelines alongside unit tests. Manual verification with Debugbar remains valuable for exploratory testing, but automated assertions prevent fixed N+1s from returning during refactors or dependency upgrades across long-lived projects.

Developers often eager load in the wrong scope, such as loading relationships in a repository method that gets called multiple times per request, or applying with inside loops thinking it batches queries when it actually duplicates them. Another frequent error is eager loading relationships that are subsequently filtered in PHP rather than at the database level, negating the performance benefit. Review eager load placement holistically across the request lifecycle. On production Laravel applications I maintain, moving eager loads from service methods to controller-level composition eliminated redundant queries that persisted despite correct with syntax. Trace the full execution path, not just individual method calls.

Share this article

Quick Contact Options
Choose how you want to connect me: