{"post":{"slug":"graphql-n-plus-one-2026","title":"GraphQL N+1: When a Flexible API Melts the Database","excerpt":"A query requesting a list and a nested field per item generates one database query per item. GraphQL makes this trivially easy to trigger from the client side.","category":"code","date":"January 6, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span> 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.\n\nTwenty-one queries for a request that could have been two. And the client wrote a query that looks entirely innocuous.\n\n## What changed in 2026\n\n- **Batching became standard infrastructure.** Every mature GraphQL implementation ships with or expects a batching layer.\n- **Complexity limits became mandatory for public APIs.** Exposing GraphQL without cost controls was recognised as a denial-of-service surface.\n- **Persisted queries spread.** Restricting clients to pre-approved queries removed the arbitrary-cost problem for first-party clients.\n- **Cost analysis tooling matured.** Static analysis of query cost before execution became practical.\n\n## Why resolvers produce N+1\n\nGraphQL executes by resolving fields. A field returning a list resolves once. A field on each item of that list resolves once per item.\n\nThat 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.\n\nNesting 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.\n\n## Batching\n\nThe standard solution collects resolver requests within an execution tick and issues one query for all of them.\n\nRather 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.\n\nTwenty-one queries becomes two. The resolvers are unchanged; the loading is batched underneath.\n\nTwo 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.\n\nBatching also deduplicates. Ten posts by the same author produce one identifier in the batch rather than ten queries.\n\n## The deeper problem: client-controlled cost\n\nBatching fixes the query count. It does not fix the fact that **the client decides how expensive a request is.**\n\nA 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.\n\nThat makes cost controls mandatory rather than advisable for any publicly exposed GraphQL API:\n\n| Control | Prevents |\n| --- | --- |\n| Depth limit | Deeply nested queries |\n| Complexity scoring | Expensive queries regardless of depth |\n| Pagination requirement | Unbounded list results |\n| Rate limiting by cost | Many moderately expensive queries |\n| Persisted queries | Arbitrary queries entirely |\n\nPersisted 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.\n\n## Common mistakes\n\n- **No batching layer.** N+1 on every nested field.\n- **A batcher shared across requests.** Cross-request data leakage.\n- **Incomplete batch keys.** Results mixed across tenants or scopes.\n- **No depth or complexity limits on a public API.** A denial-of-service surface.\n- **Unbounded list fields.** A single field can return everything.\n- **Assuming batching fixes cost.** It fixes query count, not total work.\n- **Not monitoring resolver-level timing.** Hard to attribute slowness without it.\n\n## FAQ\n\n**Does batching solve N+1 completely?**\nFor query count, largely yes. Total work remains client-controlled, which is why limits are still required.\n\n**Are depth limits enough?**\nNot alone — a shallow query over enormous lists is expensive without being deep. Complexity scoring, which accounts for list sizes, is the more complete control.\n\n**Should I use persisted queries?**\nFor first-party clients, yes — it removes arbitrary cost entirely. For a public API where third parties need flexibility, it defeats the purpose.\n\n**Is this GraphQL-specific?**\nThe N+1 shape is not — it appears in any ORM-backed code, per [ORM pitfalls](/blog/orm-pitfalls-explained-2026). What GraphQL adds is that clients trigger it directly.\n\n## Where to go next\n\nFor the same problem in application code, read [ORM pitfalls](/blog/orm-pitfalls-explained-2026). For finding it in production, [pg_stat_statements](/blog/pg-stat-statements-2026), and for rate limiting by cost, [API rate limiting](/blog/api-rate-limiting-2026).","hue":320,"metaTitle":"GraphQL N+1: When a Flexible API Melts the Database","metaDescription":"Why GraphQL resolvers naturally produce N+1 queries, how batching solves it, and why depth and complexity limits are mandatory on public APIs.","keywords":["graphql n plus one","dataloader batching","resolver performance","query complexity limits","graphql depth limit","api cost control","nested query cost","api design 2026"],"tldr":["**Resolvers run per field per item** — which is exactly the shape that produces N+1.","**Batching is the standard fix** — collect requests within a tick and issue one query.","**Clients control query shape, so cost is client-controlled** — which is the deeper problem.","**Depth and complexity limits are required, not optional** — a nested query can be arbitrarily expensive.","**Skip:** exposing a GraphQL API publicly without cost analysis. The client decides how expensive the request is."],"readTime":"4 min read","wordCount":805},"related":[{"slug":"n-plus-one-query-problem-2026","title":"The N+1 Query Problem Explained in 2026","excerpt":"The N+1 query problem quietly turns one query into hundreds by firing a fetch per row inside a loop. Here is how to spot it, and the fixes that actually work.","category":"code","date":"July 2, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":280},{"slug":"graphql-vs-rest-2026","title":"GraphQL vs REST in 2026: the honest tradeoff guide","excerpt":"GraphQL solves real over-fetching and under-fetching problems, but it introduces new ones. Here is when it earns its complexity in 2026.","category":"code","date":"April 13, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":320},{"slug":"rest-vs-graphql-2026","title":"REST vs GraphQL in 2026: which API style should you build?","excerpt":"REST is still the default for most APIs, but GraphQL has found its genuine niche. This 2026 guide gives you the honest trade-offs and a concrete decision framework.","category":"code","date":"April 7, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":307}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"filter-replacement-schedule-2026","title":"Filter Replacement: The Maintenance Nobody Tracks","excerpt":"Filters are cheap, replacing them is easy, and forgetting shortens the life of expensive equipment. The obstacle is entirely that nobody remembers when they were last changed.","category":"guides","date":"January 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":145},"older":{"slug":"headphone-impedance-explained-2026","title":"Headphone Impedance: Why Some Need an Amplifier","excerpt":"Impedance and sensitivity together determine whether a headphone plays loudly enough from a phone. Neither number alone tells you, which is why the specification is so often misread.","category":"tech","date":"January 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":242}}