Unravel guide

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.

Debug it in production.

Connect your real app in two minutes. Free forever on a 3-day window: the full debugger, not a demo.

Start free