Q8 of 20 · GraphQL

What is the N+1 problem in GraphQL, and how would you test for it?

GraphQLSeniorgraphqlperformancen-plus-onesenior

Short answer

Short answer: A list query with a nested field can fire one backend call per item — 1 for the list, then N for the nested field. It's invisible to functional assertions (the response is correct) and surfaces as latency under load. Test by measuring resolver/DB call counts against list size, not by checking the response body.

Detail

The N+1 problem is the signature GraphQL performance bug. Consider:

query {
  orders(first: 100) {
    id
    customer { name }   # naive resolver hits the DB once PER order
  }
}

A naive server resolves customer independently for each of the 100 orders: 1 query for the orders + 100 for the customers = N+1 database calls.

Why it's dangerous for QA: the functional response is completely correct. Every order has its customer name. A body-assertion test passes. The bug only shows up as latency that grows with list size — and often only under production-scale data.

How to test for it:

  1. Instrument call counts. In integration tests, assert on the number of resolver invocations or DB queries for a list query — it should not scale linearly with the number of items.
  2. Watch timing as list size grows. Query for 10, 100, 1000 items and check that latency doesn't grow worse-than-linearly in a way that signals per-item calls.
  3. Inspect with tooling. Apollo tracing / server logs reveal per-field resolver timing.

The fix is server-side — batching with a DataLoader-style pattern — but catching it is squarely a QA/performance responsibility, and it's exactly the kind of issue that passes every functional test and then takes prod down.

// EXAMPLE

// Integration-test shape: assert DB calls don't scale with list size
const dbCalls = await countDbQueries(() =>
  client.query({ query: ORDERS_WITH_CUSTOMER, variables: { first: 100 } })
);
// Naive (N+1) server: ~101 calls. Batched (DataLoader): ~2.
expect(dbCalls).toBeLessThan(5);

// WHAT INTERVIEWERS LOOK FOR

Recognising N+1 is invisible to functional tests, that it surfaces under load, and proposing call-count/timing measurement rather than body assertions.

// COMMON PITFALL

Calling N+1 'covered' because the functional test passes — the response is right; the cost is in call count.