Your Laravel code works perfectly, but your API is slow. Here’s one common reason why — and how to fix it.

You build a Laravel application.

Everything works perfectly on your local machine. Your controllers look clean, your Eloquent relationships work correctly, and your API returns the expected response.

Then your application goes live.

Suddenly, one API takes several seconds to respond. A page executes hundreds of database queries, and performance gets worse as your data grows.

You inspect the controller and find code that looks completely normal:

$posts = Post::all();

foreach ($posts as $post) {
echo $post->user->name;
}

Nothing looks wrong.

But behind the scenes, Laravel might be executing far more database queries than you expect.

Welcome to the N+1 Query Problem.

What Is the N+1 Query Problem?

Imagine you’re building a blog application. You have 100 posts, and each post belongs to a user.

You want to display every post with its author’s name.

Your code retrieves the posts and accesses the associated user:

$posts = Post::all();

foreach ($posts as $post) {
echo $post->user->name;
}

The problem is that Laravel uses lazy loading by default. When you access $post->user, Eloquent may execute another query to retrieve that relationship.

With 100 posts, the database queries could look like this:

SELECT * FROM posts;

SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
-- And many more...

The result?

  • 1 query to retrieve the posts.
  • 100 additional queries to retrieve their users.

Total: 101 queries.

This is the N+1 Query Problem.

Here, N represents the number of posts retrieved by the first query.

The exact number of queries depends on your data and relationships, but the underlying problem remains the same: repeatedly querying related data instead of loading it efficiently.

The Solution: Eager Loading

Laravel provides a straightforward solution called eager loading.

Instead of retrieving relationships one at a time, you can tell Eloquent to load them in advance.

Replace this:

$posts = Post::all();

With this:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
echo $post->user->name;
}

That’s it.

For this simple example, Laravel typically executes two queries instead of 101:

SELECT * FROM posts;

SELECT * FROM users WHERE id IN (1, 2, 3, ...);

Laravel retrieves the posts first, loads their associated users in a separate query, and matches the results to the posts.

Instead of querying the database for every post, Eloquent can retrieve the related users in a batch.

One small change can dramatically reduce unnecessary database queries.

How Does Eager Loading Work?

Let’s look at a typical Eloquent relationship.

Our Post model belongs to a user:

class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}

Without eager loading:

$posts = Post::all();

Each time you access $post->user, Laravel may query the database for that user's information.

With eager loading:

$posts = Post::with('user')->get();

Laravel loads the relationship before your loop accesses it.

This is why understanding Eloquent relationships is important — not just for writing clean code, but also for building performant applications.

What If You Need Multiple Relationships?

Real-world Laravel applications rarely have just one relationship.

Imagine an e-commerce application where you need to display orders, customers, and products.

You can eager-load multiple relationships like this:

$orders = Order::with([
'user',
'products',
])->get();

Now you can access the related data:

foreach ($orders as $order) {
echo $order->user->name;
foreach ($order->products as $product) {
echo $product->name;
}
}

Eloquent can retrieve these relationships in batches rather than issuing a new query for each order.

You can also load nested relationships:

$posts = Post::with([
'user',
'comments.user',
])->get();

This loads posts, their authors, comments, and the users associated with those comments.

Nested eager loading is particularly useful when creating APIs that return structured JSON responses.

Don’t Retrieve More Data Than You Need

Eager loading helps reduce queries, but loading every related record isn’t always the best approach.

Suppose you want to display a list of posts with the number of comments on each post.

You don’t need to retrieve every comment just to count them.

Instead of:

$posts = Post::with('comments')->get();

Use:

$posts = Post::withCount('comments')->get();

Now you can access the comment count:

foreach ($posts as $post) {
echo $post->comments_count;
}

Laravel adds a comments_count attribute without loading all the comment models into memory.

This is especially useful when a post has thousands of comments.

The goal isn’t simply to reduce query count. It’s to retrieve only the data your application actually needs.

Combine Eager Loading With Pagination

Consider this query:

$posts = Post::with('user')->get();

It avoids the N+1 problem, but what happens when your database contains 100,000 posts?

You would still retrieve a very large collection of records.

Eager loading alone doesn’t solve that problem.

For an API or listing page, pagination is usually a better choice:

$posts = Post::with('user')
->latest()
->paginate(20);

Now Laravel retrieves one page of posts and loads their associated users.

This helps control memory usage and avoids processing unnecessary records.

For very large datasets, Laravel also supports cursor pagination through cursorPaginate(), which can be useful when navigating records in a stable order.

Choose the pagination strategy based on your application’s requirements.

Select Only the Columns You Need

Another useful optimization is to limit the columns retrieved from the database.

For example:

$posts = Post::select([
'id',
'user_id',
'title',
])
->with('user:id,name')
->latest()
->paginate(20);

Here, we retrieve only the fields needed to display the post title and author name.

Notice that we include user_id in the post query and id in the user query. These keys are important because Eloquent uses them to match related records.

This approach is particularly useful when working with large tables or API responses that don’t require every column.

How Can You Detect N+1 Queries?

Instead of guessing why your application is slow, inspect the actual database queries.

Here are three practical options.

1. Laravel Debugbar

Laravel Debugbar helps you inspect queries executed while loading a page. Look for repeated queries that differ only by a related record’s ID.

2. Laravel Telescope

Telescope helps you investigate requests and database queries during development, making repeated query patterns easier to spot.

3. Database query logging

Laravel provides a query listener that can help you investigate database performance:

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;

DB::listen(function (QueryExecuted $query) {
logger()->debug('Database query', [
'sql' => $query->sql,
'time_ms' => $query->time,
]);
});

Use query logging carefully in production. Logging every SQL query can create significant overhead, and logs should not expose sensitive information.

The important lesson is simple: measure before optimizing.

Prevent Lazy Loading During Development

Laravel also provides a feature to help developers catch unexpected lazy loading.

You can enable it in your AppServiceProvider:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
Model::preventLazyLoading(
! $this->app->isProduction()
);
}

This configuration helps identify relationships that are accessed without being explicitly loaded in non-production environments.

For example, accessing $post->user without eager loading may trigger a lazy-loading violation.

This is particularly useful when working on large Laravel applications where a seemingly harmless relationship access can introduce unnecessary queries.

Test this behavior with your application’s Laravel version and development setup before relying on it in your workflow.

Does Eager Loading Always Improve Performance?

Not necessarily.

Consider this query:

$posts = Post::with([
'user',
'comments',
'likes',
'categories',
'tags',
])->get();

What if your page only displays the post title and author’s name?

Loading comments, likes, categories, and tags would retrieve unnecessary data.

A better approach is to load only what the current page requires:

$posts = Post::with('user:id,name')
->select([
'id',
'user_id',
'title',
])
->latest()
->paginate(20);

Good query optimization is about balance.

You want to reduce unnecessary database calls without retrieving so much related data that memory usage and response times get worse.

A Practical Checklist for Laravel Developers

Before deploying your Laravel application, ask yourself:

  • Am I accessing Eloquent relationships inside loops?
  • Should I use with() or nested eager loading?
  • Can I use withCount() instead of retrieving every related record?
  • Am I selecting unnecessary database columns?
  • Should the results be paginated?
  • Have I inspected the actual SQL queries?
  • Can lazy-loading prevention help catch mistakes during development?

These small checks can prevent performance issues from reaching production.

Final Thoughts

The N+1 Query Problem is one of those Laravel performance issues that can hide in plain sight.

Your code may be readable. Your Eloquent relationships may work correctly. Your API may return exactly what you expect.

Yet your application could still execute hundreds of unnecessary database queries.

Fortunately, the solution often begins with understanding how Eloquent loads relationships.

Remember these three concepts:

Lazy Loading — retrieves a relationship when it is accessed.

Eager Loading — retrieves relationships in advance using methods such as with().

Query Optimization — retrieves only the data your application needs, using techniques such as withCount(), selected columns, and pagination.

The next time your Laravel API becomes slow, don’t immediately blame the server or increase its resources.

First, inspect your database queries.

Sometimes, a simple with() is all it takes to eliminate an inefficient query pattern.

Write clean code. Measure performance. Optimize based on evidence.

What other Laravel performance issues have you encountered in production? Share your experience in the comments.

Happy Coding! 🚀

— Mr. Chauhan