‹ Back to Blog

Detecting N+1 Queries in Prisma and Node.js with Scout

Engineering Node.js Performance

N+1 queries are the performance bug that passes every code review. In the Prisma and Node.js ecosystem, the pattern is especially easy to introduce and surprisingly hard to catch before it reaches production.

What an N+1 Looks Like in Prisma

The classic N+1 happens when you fetch a list of records, then query related data for each one individually. Here is the pattern in Prisma:

// The N+1 pattern: 1 query for users + N queries for posts
const users = await prisma.user.findMany();

const usersWithPosts = await Promise.all(
  users.map(async (user) => {
    const posts = await prisma.post.findMany({
      where: { authorId: user.id },
    });
    return { ...user, posts };
  })
);

This code reads fine. It does exactly what it says. But for 100 users, you are executing 101 database queries. For 1,000 users, that is 1,001 queries.

Why Prisma Makes This Easy to Write

Prisma does not use lazy loading by default, which is actually a good design decision. Lazy loading in ORMs like ActiveRecord or Hibernate silently fires queries when you access a relation, making N+1s even harder to spot. Prisma forces you to be explicit about what data you load.

But the “loop and query” pattern still feels natural. When you need related data, you write a query for it. When that query lives inside a loop or a .map(), you have an N+1. The code looks correct because each individual query is intentional. Nothing in your editor warns you that the same query template is about to execute hundreds of times.

Why You Do Not Catch Them in Development

Your local database has 10 users. Each user has 3 posts. The entire endpoint responds in 15ms. There is nothing in that response time to raise concern.

Even with Prisma’s query logging enabled, the output scrolls by quickly in development. You see a handful of SELECT statements, nothing alarming. The N+1 is there, but at small scale it is invisible.

Code review rarely catches these either. The query inside the loop is often in a separate service method, abstracted away from the iteration that calls it:

// In userService.ts
async function enrichUserProfile(userId: string) {
  const posts = await prisma.post.findMany({
    where: { authorId: userId },
  });
  const notifications = await prisma.notification.findMany({
    where: { userId, read: false },
  });
  return { posts, notifications };
}

// In the route handler, the N+1 is hidden
const users = await prisma.user.findMany({ where: { active: true } });
const profiles = await Promise.all(users.map((u) => enrichUserProfile(u.id)));

The reviewer sees enrichUserProfile and moves on. Two N+1s are now shipping.

What Happens in Production

Production has real data. A hundred active users means 301 queries on that endpoint: 1 for users, 100 for posts, 100 for notifications. Each query carries network overhead to the database, connection pool contention, and serialization cost.

A response that took 50ms locally now takes 2 seconds. Under load, the connection pool saturates and requests start queuing. Your p99 latency spikes. Users see a loading spinner where they used to see instant results.

How Scout Detects N+1 Queries Automatically

Scout Monitoring instruments Prisma at the engine level. With Prisma 6, Scout leverages the native traceparent propagation to capture spans directly from Prisma’s query engine. Every database call is recorded with its query pattern, duration, and the code location that triggered it.

When Scout sees the same query template executed repeatedly within a single request, it identifies the N+1 pattern. The trace view shows the repeated queries stacked together, with a direct link to the line of code responsible. You do not need to add custom logging or enable verbose query output. Scout surfaces the problem automatically, ranked by time impact, so you fix the most expensive ones first.

This works for every Prisma model and query type. Whether it is findMany, findUnique, or a raw query inside a loop, every call shows up in the trace with its timing and origin.

How to Fix Them

The most straightforward fix is Prisma’s include option, which tells Prisma to fetch related data in a single query using a SQL join:

// Fixed: 1 query with a join instead of 101 queries
const usersWithPosts = await prisma.user.findMany({
  include: {
    posts: true,
  },
});

For more complex cases where you need to reshape the data or apply filters to relations, use select with nested conditions. Batch queries with where: { id: { in: userIds } } work well when include does not fit.

If you are building a GraphQL API, the DataLoader pattern prevents N+1s at the resolver level:

import DataLoader from 'dataloader';

const postLoader = new DataLoader(async (userIds: readonly string[]) => {
  const posts = await prisma.post.findMany({
    where: { authorId: { in: [...userIds] } },
  });
  const postsByUser = new Map<string, Post[]>();
  posts.forEach((post) => {
    const existing = postsByUser.get(post.authorId) || [];
    postsByUser.set(post.authorId, [...existing, post]);
  });
  return userIds.map((id) => postsByUser.get(id) || []);
});

// In your resolver
const resolvers = {
  User: {
    posts: (user) => postLoader.load(user.id),
  },
};

DataLoader collects all the individual .load() calls within a single tick of the event loop and batches them into one query. This is the standard approach for GraphQL servers running on Node.js.

Monitoring For New N+1s Over Time

Fixing existing N+1s is half the battle. New code paths introduce new ones. A feature that adds a “recent activity” sidebar, a reporting endpoint that aggregates data across accounts, a webhook handler that processes records in a batch. Each is an opportunity for the pattern to reappear.

Scout Monitoring continuously watches for repeated query patterns across all your endpoints. When a new N+1 appears in a recently deployed code path, Scout flags it. You see it in context: which endpoint, which line of code, how many times the query repeated, and how much time it added to the response. That feedback loop between deploy and detection is what keeps N+1s from quietly degrading your application over months.

Getting started takes a few minutes. Install the Scout agent, configure your Prisma client, and deploy. There is nothing else to set up.

Try Scout Monitoring for Node.js. Free tier, no credit card required.

For application monitoring with errors and traces, Scout Monitoring provides the fastest insights without the bloat.