Q8 of 20 · GraphQL
What is the N+1 problem in GraphQL, and how would you test for it?
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:
- 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.
- 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.
- 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
// COMMON PITFALL
// Related questions