Skip to content

The N+1 Problem That’s Killing Your App Performance (And How I Fixed It)

Spoiler: It wasn’t my server specs.

So there I was, deploying my app — can’t drop the real name, NDA things 😅 — feeling pretty good about myself. Everything worked perfectly in development, right?

Wrong.

The moment real users started hitting my API, everything went to hell. Response times went from 200ms to 5+ seconds.

My database was screaming.

Users were complaining.

I was panicking.

“But it works fine on my machine!” 🤦‍♂️

Turns out, I was making the classic N+1 mistake. And honestly? I had no idea what that even meant until I spent 3 days debugging this mess.

What the Heck is N+1?

Picture this: You’re fetching 100 courses from your database.

Sounds simple, right?

What I was doing (the wrong way):

// ❌ This looks innocent but it's a performance killer
const courses = await Course.find({}); // 1 query

for (let course of courses) {
    const creator = await User.findById(course.createdBy); // 100 more queries!
    course.creator = creator;
}

Total queries: 101 😱

That’s 101 round trips to your database.

Each one takes time.

Multiply that by concurrent users, and boom – your server is toast.

Here’s what my server logs looked like:

Query 1: Find courses (200ms)
Query 2: Find user for course 1 (50ms)
Query 3: Find user for course 2 (50ms)
Query 4: Find user for course 3 (50ms)
... (you get the idea)

Total time: 5.2 seconds for what should have been a 200ms request.

What I should have done:

// ✅ One query with a join - much better!
const courses = await Course.find({})
    .populate('createdBy', 'firstName lastName email'); // Just 1 query!

Total queries: 1 🎉

The 3 Fixes You Can Implement Today

1. Use Populate (Mongoose)

// Instead of this mess:
const courses = await Course.find({});
for (let course of courses) {
    course.creator = await User.findById(course.createdBy);
}

// Do this:
const courses = await Course.find({})
    .populate('createdBy', 'firstName lastName email');

    2. Add Database Indexes

    // In your model file:
    courseSchema.index({ createdBy: 1 }); // Index foreign keys
    courseSchema.index({ status: 1, createdAt: -1 }); // Compound indexes

    3. Use Aggregation for Complex Queries

    // When you need multiple joins:
    const result = await Course.aggregate([
        {
            $lookup: {
                from: 'users',
                localField: 'createdBy',
                foreignField: '_id',
                as: 'creator'
            }
        }
    ]);

    The Real Impact

    After fixing this, my app went from handling 50 concurrent users to 500+.

    Same server, same codebase, just better queries.

    Performance improvement: 96% faster

    User capacity: 10x increase

    My sanity: Restored 😅

    What’s Your Experience?

    Have you run into the N+1 problem?

    What was your “oh shit” moment when you realized your queries were the bottleneck?

    Drop a comment below – I’d love to hear your war stories!


    If you want to understand different loop patterns in JavaScript, check out my earlier post:
    👉 Confused About JavaScript Loops? We Can Help.

    And if you want a deeper dive into the N+1 issue, this article is a great read:
    👉 What Is the N+1 Problem — JavaScript Edition (Accreditly)

    Leave a Reply

    Your email address will not be published. Required fields are marked *