One of the most common Laravel performance problems I have seen is the N+1 query problem.
The interesting thing is that the code usually looks completely normal.
The application works.
The page loads.
The result is correct.
But when the number of records increases, the same code can become very slow.
This is especially important in ERP, CRM, SaaS and other applications where a page can display hundreds or thousands of records.
What is the N+1 problem?
Let's take a simple example.
Suppose we have users and their companies.
We fetch 100 users:
$users = User::get();Then inside the Blade file:
@foreach ($users as $user)
{{ $user->company->name }}
@endforeachAt first, this looks completely fine.
But Laravel may execute:
1 query → get users
100 queries → get each user's companySo the application can execute:
101 queriesfor only 100 users.
This is the N+1 problem.
Why is it called N+1?
The first query gets the main records.
Then another query is executed for each record.
For example:
1 + 100 = 101 queriesIf there are 1,000 users:
1 + 1,000 = 1,001 queriesThe problem becomes much bigger as the dataset grows.
The simple Laravel solution
Laravel Eloquent provides eager loading.
Instead of:
$users = User::get();we can use:
$users = User::with('company')->get();Now Laravel can load the required company records together instead of querying the database separately for every user.
The important difference is not just the number of lines of code.
It can make a large difference to database load.
This problem is easy to miss
One reason N+1 queries are dangerous is that they may not appear during development.
Imagine your local database has only:
20 usersThe page may load very quickly.
After the application goes live:
20,000 usersNow the same code can create a large number of database queries.
This is why I always think about query behaviour, not only whether the code works.
Check your relationships
For example:
class Order extends Model
{
public function customer()
{
return $this->belongsTo(Customer::class);
}
}If we display customer information for many orders:
$orders = Order::get();and later:
foreach ($orders as $order) {
echo $order->customer->name;
}we should consider eager loading:
$orders = Order::with('customer')->get();The same concept applies to:
User → Company
Order → Customer
Order → Items
Product → Category
Invoice → Customer
Employee → Department
Post → AuthorAny relationship accessed repeatedly can potentially create this problem.
Nested relationships can create more queries
It is not only one relationship.
For example:
Order::with('customer.company')->get();can be useful when the page needs both:
Order
↓
Customer
↓
CompanyInstead of loading relationships later inside loops.
For larger applications, I prefer deciding what data is actually required before writing the query.
Don't use eager loading blindly
There is another side to this.
Eager loading everything is not automatically better.
For example:
User::with([
'company',
'roles',
'permissions',
'orders',
'addresses',
'activities',
'notifications'
])->get();This may load far more data than the page actually needs.
So the goal is not:
Always use
with().
The goal is:
Load the data that the current operation actually needs.
This is especially important for large datasets.
Select only required columns
Suppose we only need:
User ID
User name
Company nameThere is no reason to load every column from a large table.
For example:
$users = User::select([
'id',
'company_id',
'name'
])->with([
'company:id,name'
])->get();This can reduce the amount of data transferred from MySQL to PHP.
For large applications, small improvements like this can matter.
Pagination is also important
Another mistake is loading thousands of records at once.
For example:
$users = User::with('company')->get();If there are 500,000 users, this is not a good approach for a normal web page.
Use pagination when the UI only needs a page of results:
$users = User::with('company')->paginate(50);Now the application only needs to work with a limited number of records for that request.
Don't fix N+1 only in Blade
Sometimes developers notice that Blade is causing extra queries and try to fix the Blade code.
I prefer solving the data requirement at the query level.
Instead of:
$orders = Order::get();and then accessing relationships repeatedly in the view, prepare the required data before passing it to the view.
For example:
$orders = Order::with('customer')
->latest()
->paginate(50);Then the view can simply display the already-loaded relationship.
This makes the data flow easier to understand.
API responses can have the same problem
N+1 is not only a Blade problem.
It can also happen in APIs.
For example:
return UserResource::collection(
User::get()
);If the resource accesses:
$this->company->namewithout the relationship being loaded, the API can generate many additional queries.
This is especially important when an API is used by a mobile application.
The API may return the correct response, but the backend can be doing a huge amount of unnecessary database work.
Use query logs while developing
When I am checking a performance issue, I don't only look at the PHP code.
I want to know what SQL is actually being executed.
Laravel provides tools that can help identify queries during development.
For example, query logging can show whether a loop is unexpectedly generating hundreds of queries.
Database monitoring tools can also help when debugging a real production issue.
The important thing is to measure instead of guessing.
Indexes are another part of the problem
Sometimes developers try to solve every performance problem with Eloquent changes.
But the database itself also needs to be designed properly.
For example, if we frequently query:
Order::where('company_id', $companyId)->get();then the database index on company_id can be important.
The same applies to columns commonly used for:
WHERE
JOIN
ORDER BY
GROUP BYThe correct indexes depend on the actual queries and data distribution.
Adding indexes everywhere is not the answer either.
Indexes also have storage and write-performance costs.
Large SaaS applications need extra care
For a multi-tenant SaaS application, database queries can become much more important.
Imagine:
Company A → 10,000 orders
Company B → 500,000 orders
Company C → 2,000,000 ordersA query that works perfectly for Company A may become slow for Company C.
This is why I prefer testing queries against realistic data volumes.
Don't only test with 100 records if the production system may eventually contain millions.
My basic checklist for Laravel queries
When working on a large Laravel application, I normally check:
✓ Is there an N+1 query?
✓ Are relationships eager loaded?
✓ Are we loading unnecessary relationships?
✓ Are we selecting unnecessary columns?
✓ Is pagination required?
✓ Are the WHERE columns indexed?
✓ Are JOIN conditions indexed?
✓ Is sorting expensive?
✓ Is the dataset realistic?
✓ How many SQL queries are executed?
✓ How much data is returned?These checks can catch many performance problems before they become production problems.
Final thoughts
N+1 queries are a good example of how a few lines of perfectly valid Laravel code can create a serious performance problem later.
The code may work with 20 records.
It may still work with 500 records.
But when the application grows to hundreds of thousands or millions of records, database behaviour becomes much more important.
For me, good Laravel development is not only about writing clean Eloquent code.
It is also about understanding what that code is doing to MySQL.
Always look beyond:
"Does it work?"and also ask:
"How many queries does it generate?"
"How much data does it load?"
"What happens when the table becomes much larger?"That mindset becomes especially important when building large ERP, CRM and SaaS applications.