All guides

LEARN \u00b7 DEBUGGING GUIDE

N+1 query slowing down your API: how to find and fix it

You load a list of 50 orders. Your ORM issues one query for the orders. Then, for each order, it issues a query for the customer, and another for the line items. 1 + 50 + 50 = 101 queries. The page takes 3 seconds.

IntermediateAPI/backend debugging

What this usually means

The N+1 problem: you fetch a list of N items with one query. Then, for each item, you access a related field (e.g. `order.customer.name`). The ORM issues a query to fetch that related data — for every single item. This happens because the ORM lazy-loads relations by default. It does not know you are about to access the related data for all items, so it fetches one at a time. The fix is eager loading: tell the ORM upfront which relations to include so it fetches everything in 1-3 queries with JOINs or IN clauses.

( 01 )Fast diagnosis

The first ten minutes \u2014 establish facts before touching code.

  • 1Enable query logging on your ORM. Count the queries per request. If it is 1 + N (where N is the list size), you have an N+1.
  • 2Look at the code that iterates over the result list. If it accesses a relation or nested property, that is where the extra queries come from.
  • 3Check if your ORM has an N+1 detection tool. Some ORMs (Prisma, ActiveRecord, Django) have built-in detection or plugins.
  • 4Use an APM tool to trace the request. Look for many fast database queries clustered together.
  • 5Run the request on a small dataset (10 items) and a large dataset (1000 items). If the query count scales with dataset size, N+1 is likely.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchORM query logs — count queries per request and identify repeated patterns
  • searchCode that iterates over a list and accesses related objects
  • searchORM eager loading configuration — `.include()`, `.with()`, `.prefetch_related()`
  • searchDatabase slow query log — look for many small queries between two larger queries
  • searchAPI endpoint handler — trace from route to view to data layer
  • searchGraphQL resolvers — DataLoader or similar batching library
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningLazy loading: the ORM fetches related data on access, one item at a time
  • warningMissing eager load directive: `.include('customer')` or `.prefetch_related('items')` not used
  • warningNested N+1: eager loading one level but missing a nested relation
  • warningSerializer or view that accesses related fields without preloading
  • warningGraphQL resolver that loads related data per item instead of using a DataLoader
  • warningLoop that calls a service method which makes its own database query for each item
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAdd eager loading: in Sequelize, use `.include()`. In Prisma, use `.include()`. In Django, use `.select_related()` or `.prefetch_related()`. In Rails, use `.includes()`.
  • buildFor GraphQL, use DataLoader to batch and cache database queries within a single request.
  • buildUse a query builder with JOINs instead of relying on the ORM to fetch related data separately.
  • buildAdd an N+1 detection lint rule or CI check that fails the build if query count exceeds a threshold.
  • buildWrite a custom query that fetches all needed data in one round trip with JOINs and returns a flat or nested structure.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun the request with query logging. Query count should drop from ~N+1 to ~1-5.
  • verifiedBenchmark the endpoint before and after. Latency for a list of 100 items should be sub-200ms, not 2+ seconds.
  • verifiedTest with different list sizes (10, 100, 1000 items). Query count should stay constant, not scale.
  • verifiedAdd an automated test that asserts query count per request is below a threshold.
  • verifiedReview the code for any remaining lazy loads — nested relations are easy to miss.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningFixing N+1 by adding a cache instead of fixing the query — the first request is still slow
  • warningEager loading everything 'just in case' — this over-fetches data and creates slow queries for a different reason
  • warningNot checking for nested N+1 — eager loading the parent but not the grandchild
  • warningAssuming the ORM handles this automatically — most ORMs default to lazy loading
  • warningNot measuring query count as part of performance testing