N+1 queries silently kill performance
You fetch 100 records. Then loop through them. Each iteration triggers another query. 1 + 100 = 101 queries instead of 2.
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
}N+1 queries are the most common performance issue in Laravel applications. They happen when you fetch a collection of records, then access a relationship on each one. Laravel's Eloquent makes this feel seamless—you just write $post->user—but behind the scenes, it's executing a separate database query for every single iteration.
The pattern is so common because it doesn't show up in development. With 10 records in your local database, those extra queries barely register. But in production, with thousands of records and concurrent users, those 1001 queries become a database meltdown.
What N+1 looks like in practice
The cost adds up fast
Each database query has overhead: connection pool management, query parsing, execution planning, network round-trip, result serialization. When you're doing this 47 times instead of 2, you're not just spending 23x more time—you're also consuming 23x more database connections, leaving less capacity for other requests.
In a production environment with concurrent users, this creates a cascading effect. Slow queries lead to connection pool exhaustion, which leads to request queuing, which leads to timeouts, which leads to frustrated users.
Detect with one command
Flags queries repeating 3+ times with exact source location
$ php artisan perf:query --n1=3{
"n1": {
"candidates": [
{
"count": 47,
"table": "contacts",
"normalized_sql": "select * from \"contacts\" where \"id\" = ?",
"example_source": [
{
"file": "app/Domains/Deals/Resources/DealResource/Pages/ListDeals.php",
"line": 47,
"function": "getTableQuery"
}
]
}
]
}
}The --n1=3 flag tells laraperf to flag any SQL pattern that repeats 3 or more times. You can adjust this threshold—--n1=2 catches more potential issues, --n1=5 focuses on only the most egregious cases.
The output includes the exact file path and line number where the N+1 originates. Not a stack trace buried in vendor code—the actual line in your application where you called $post->user or similar. This precision is what makes automated fixing possible.
The fix is one line
// ❌ N+1 - queries = 1 + N
$posts = Post::all();
foreach ($posts as $post) {
$post->user->name; // Query #2, #3, #4...
}// ✅ Eager loading - queries = 2
$posts = Post::with('user')->get();
foreach ($posts as $post) {
$post->user->name; // Already loaded!
}The with() method tells Eloquent to fetch the relationship in the original query using a JOIN or a separate query with WHERE IN. Both are dramatically more efficient than individual queries per record.
For nested relationships, you can eager load multiple levels: Post::with('user.company'). You can also eager load multiple relationships: Post::with(['user', 'comments']). The principle is the same—load all the data you need upfront, not on demand.
What you get
Exact line numbers
File path and line where the N+1 originates in your code, not buried in vendor frames
Query hash
Reference identical queries across different sessions and track them over time
Occurrence count
See exactly how many times each query repeated—no guessing about severity
Table name
Know which table is being queried repeatedly to prioritize fixes
Beyond eager loading
Sometimes eager loading isn't the right solution. If you only need the user name for 2 out of 100 posts, eager loading all 100 users wastes memory. In these cases, consider:
Selective loading: Use when($needsUser, fn($q) => $q->with('user')) to conditionally eager load.
Lazy eager loading: Call $posts->load('user') after filtering to eager load only the records you actually need.
Data transfer objects: If you only need specific fields, use Post::with(['user:id,name']) to limit what gets loaded.
Let your agent fix it automatically
The JSON output includes file:line. Your LLM agent can navigate, read context, and apply eager loading without human intervention.
This workflow transforms N+1 detection from a manual debugging chore into an automated maintenance task. The agent doesn't just find the problem—it understands the relationship from your code context, applies the appropriate eager loading syntax, and verifies the query count actually decreased.
You can run this in CI to catch N+1 regressions before they reach production. Add it to your GitHub Actions workflow, and any PR that introduces an N+1 query will fail the build with the exact location and a suggested fix.
Get started
Two ways to install — manual or let your agent handle it.
Manual install
composer require mateffy/laraperf --devIf you have Laravel Boost installed, run the following after installing laraperf — the skill is automatically added.
php artisan boost:updateLet your agent do it
Install the skill permanently with the CLI, or paste a prompt for a one-shot setup.
npx skills add mateffy/laraperfOr paste this prompt for a quick one-shot:
Using Laravel Boost? Run php artisan boost:update after installing — the skill is added automatically.