Your Laravel app is running 400 queries per page. Here's how to find out.
The most expensive bug in your Laravel application isn't a security hole. It's a missing ->with().
We've seen this enough times to have a script for the conversation. The client says the app is slow and the hosting bill keeps growing. The team says it needs better infrastructure. What it actually needs is Debugbar installed for 10 minutes.
Why N+1 is invisible until it isn't
N+1 is invisible in development. Your local database has 40 users, 60 orders, maybe 100 products. The extra query per record costs you milliseconds. You ship it. Production has 15,000 orders. The page that loaded in 400ms is now taking 6 seconds. Nobody connects the slowdown to a relationship that was always there.
The relationship didn't break. It scaled wrong.
This happens because developers test on seed data, and seed data is small. There's no performance feedback in local development until you deliberately install one. Most teams never do.
How to measure the real number
Install Laravel Debugbar locally, then seed your database to production scale. Not 50 records. At least 500, ideally whatever your P90 dataset looks like.
php artisan db:seed --class=LoadTestSeederLoad the five pages with the most traffic. Look at the query count. The numbers to know:
- Under 20: Healthy. Someone thought about this.
- 20-60: Addressable. Probably one or two relationships missing
->with(). - 60-200: Systematic. N+1 is a pattern throughout the codebase, not an isolated case.
- 200+: The application was never tested with real data.
We once audited a dashboard page running 847 queries. The client had been paying for a larger server for eight months.
The fix, and when it's not simple
For standard relationships, eager loading is one line:
// Before: one query per post to load the author
$posts = Post::all();
// After: two queries total
$posts = Post::with('author')->get();For nested relationships, chain them:
$orders = Order::with('customer.company.plan')->get();Where it gets harder: polymorphic relationships. If you have a comments table that belongs to both Post and Video, naive eager loading still generates per-type queries. Use morphWith where available, or reconsider whether the polymorphism is earning its complexity.
The second trap is conditional loading inside loops. If you're accessing a relationship inside an if block or based on a model property, eager loading can't predict the access pattern. Use loadMissing() instead:
// Won't re-query if already loaded
$posts->each(fn($post) => $post->loadMissing('tags'));The third trap is Livewire components that re-render on every user interaction without memoizing queries. Each render cycle triggers a fresh query set. This compounds fast.
What fixing it actually looks like
On a SaaS we audited last year, the main account dashboard was running 312 queries. Three causes: unguarded relationship access in a Blade component, a polymorphic activity feed with no morphWith, and a Livewire component re-querying on every render cycle.
After one day of work: 11 queries. Page load went from 4.2 seconds to 380ms. The server upgrade they had scheduled was cancelled.
Performance work doesn't require new infrastructure. It requires someone who will actually look.
We find N+1 problems in every Laravel audit we run. If you want to know where yours are, we'll find them for free.