A client requests twenty posts and each post's author. The posts resolver runs one query. Then the author resolver runs — once per post, because resolvers execute per field per item.
Twenty-one queries for a request that could have been two. And the client wrote a query that looks entirely innocuous.
What changed in 2026
- Batching became standard infrastructure. Every mature GraphQL implementation ships with or expects a batching layer.
- Complexity limits became mandatory for public APIs. Exposing GraphQL without cost controls was recognised as a denial-of-service surface.
- Persisted queries spread. Restricting clients to pre-approved queries removed the arbitrary-cost problem for first-party clients.
- Cost analysis tooling matured. Static analysis of query cost before execution became practical.
Why resolvers produce N+1
GraphQL executes by resolving fields. A field returning a list resolves once. A field on each item of that list resolves once per item.
That is the correct model for a flexible query language, and it means any nested field over a list is N+1 by default. The framework has no way to know that twenty author lookups could be one query, because each resolver is called independently with no knowledge of its siblings.
Nesting multiplies. Posts, each with an author, each with an organisation, produces N queries at the second level and N more at the third. A three-level nested query over a list of fifty items can generate thousands of database queries from a request that reads as a single sensible question.
Batching
The standard solution collects resolver requests within an execution tick and issues one query for all of them.
Rather than each author resolver querying immediately, it registers a request for an author identifier. At the end of the tick, the batching layer gathers all registered identifiers and issues one query fetching all of them, then distributes results back.
Twenty-one queries becomes two. The resolvers are unchanged; the loading is batched underneath.
Two details matter in practice. Scope the batcher per request, not globally — a batcher shared across requests caches data across users, which is a correctness and security problem. And the batch key must be complete: batching by identifier alone, when the query also depends on a tenant or a permission scope, mixes results across boundaries.
Batching also deduplicates. Ten posts by the same author produce one identifier in the batch rather than ten queries.
The deeper problem: client-controlled cost
Batching fixes the query count. It does not fix the fact that the client decides how expensive a request is.
A client can request a deeply nested query over large lists. Even with perfect batching, the total work can be enormous — and unlike REST, where each endpoint's cost is known, GraphQL's cost depends on a query the server sees for the first time at request time.
That makes cost controls mandatory rather than advisable for any publicly exposed GraphQL API:
| Control |
Prevents |
| Depth limit |
Deeply nested queries |
| Complexity scoring |
Expensive queries regardless of depth |
| Pagination requirement |
Unbounded list results |
| Rate limiting by cost |
Many moderately expensive queries |
| Persisted queries |
Arbitrary queries entirely |
Persisted queries are the strongest option where clients are first-party: the client sends an identifier for a pre-registered query rather than the query text, so the server only ever executes queries you have reviewed. That eliminates the arbitrary-cost problem completely, at the cost of the flexibility that motivated GraphQL.
Common mistakes
- No batching layer. N+1 on every nested field.
- A batcher shared across requests. Cross-request data leakage.
- Incomplete batch keys. Results mixed across tenants or scopes.
- No depth or complexity limits on a public API. A denial-of-service surface.
- Unbounded list fields. A single field can return everything.
- Assuming batching fixes cost. It fixes query count, not total work.
- Not monitoring resolver-level timing. Hard to attribute slowness without it.
FAQ
Does batching solve N+1 completely?
For query count, largely yes. Total work remains client-controlled, which is why limits are still required.
Are depth limits enough?
Not alone — a shallow query over enormous lists is expensive without being deep. Complexity scoring, which accounts for list sizes, is the more complete control.
Should I use persisted queries?
For first-party clients, yes — it removes arbitrary cost entirely. For a public API where third parties need flexibility, it defeats the purpose.
Is this GraphQL-specific?
The N+1 shape is not — it appears in any ORM-backed code, per ORM pitfalls. What GraphQL adds is that clients trigger it directly.
Where to go next
For the same problem in application code, read ORM pitfalls. For finding it in production, pg_stat_statements, and for rate limiting by cost, API rate limiting.