How to find and fix N+1 queries in Laravel
An N+1 query is when your code runs one query to fetch a list, then one more query per item to fetch a relation, so a page showing 50 orders quietly fires 51 queries instead of 2. It's the single most common cause of a slow Laravel endpoint, and it usually hides until production traffic makes it hurt.
Fixing one is easy: find the offending loop, then eager-load the relation with
with(). Finding it is the hard part. The code looks innocent, and the cost
only shows up as dozens of near-identical queries at runtime.
Spot it
The signature of an N+1 is many executions of the same query shape, differing
only by an id in the WHERE clause:
select * from "addresses" where "addresses"."user_id" = 1
select * from "addresses" where "addresses"."user_id" = 2
select * from "addresses" where "addresses"."user_id" = 3
-- …48 more
Locally you can catch this with Laravel Telescope's query tab, or by enabling
Model::preventLazyLoading() in a non-production environment so a lazy-loaded
relation throws instead of silently querying. In production, where the traffic
and data that trigger it actually live, you need a tool that captures real
requests.
Fix it
Eager-load the relation up front so it's one query, not N:
// N+1: each $order->customer fires its own query
$orders = Order::latest()->take(50)->get();
foreach ($orders as $order) {
echo $order->customer->name;
}
// Fixed: one query for orders, one for all their customers
$orders = Order::with('customer')->latest()->take(50)->get();
For nested relations use dot notation (with('customer.address')), and for
counts use withCount() instead of looping. The goal is a constant number of
queries regardless of how many rows come back.
Find it in production with Unravel
Unravel normalizes every query into a fingerprint (the query shape with its literal values stripped) and detects when one fingerprint fires many times inside a single trace. That's an N+1, and Unravel collapses the cluster into one entry on the timeline, tells you how many times it ran, and points at the exact line of your code that emitted it.
So instead of reading 51 near-identical query logs, you see: this fingerprint,
50 times, from this callsite, on this route. You add the with(), deploy, and
the cluster disappears from the next trace.
- What's captured - how fingerprints and N+1 collapsing work.
- Causality, not log soup - the interpretation Unravel does for you.
- Unravel vs Laravel Telescope - catching N+1s in production, not just locally.