GraphQL Solves a Problem You Might Not Have

Table of Contents
- The Problem GraphQL Addresses
- What You Give Up
- The N+1 Problem Is Structural
- Query Cost Is Unbounded by Default
- Authorisation Moves to Every Field
- Error Handling Is Genuinely Different
- When Each Choice Fits
- Getting Most of the Benefit From REST
- Common Pitfalls
- Conclusion
- Frequently Asked Questions
Key takeaway: GraphQL is the right choice when many diverse clients need different shapes of the same data. If you have one client, or your endpoints already return roughly what clients need, you are adopting substantial complexity to solve a problem you do not have.
The Problem GraphQL Addresses
The motivating problems are real, and worth stating precisely so the trade-off can be evaluated.
Over-fetching. A mobile client needs a user’s name and avatar. The endpoint returns forty fields including preferences, addresses, and audit metadata. On a mobile connection, that waste is measurable.
Under-fetching and waterfalls. Displaying a post with its author and comment count requires three requests, each dependent on the previous. Three round trips of latency for one screen.
Endpoint proliferation. Different clients need different shapes, so endpoints multiply — one for the list view, one for the detail view, one for the mobile summary. Each is a maintained surface.
Coupled release cycles. A client needing a new field waits for a backend deployment. With mobile app review times, that can be weeks.
GraphQL addresses all four. The client specifies exactly the fields it needs, traverses relationships in one request, and the schema serves every client shape without new endpoints. Where these problems are genuinely painful — several client platforms with diverging needs, a mobile application that cannot deploy quickly — the benefit is substantial and real.
The question is what the solution costs, and whether your situation actually has these problems.
What You Give Up
The costs are mostly infrastructure you no longer get for free.
HTTP caching. REST endpoints are cacheable by URL at the CDN, the browser, and any intermediate proxy. GraphQL queries are typically POST requests with the query in the body, which no HTTP cache can key on. Caching must be rebuilt in the application layer, which is a substantial piece of work replacing something that previously required a header.
Predictable performance. A REST endpoint’s cost is bounded by its implementation. A GraphQL query’s cost depends on what the client asked for, and clients can construct expensive queries you never anticipated.
Simple observability. Per-endpoint latency and error rates are directly available in REST. In GraphQL, every request hits one endpoint, so useful metrics require per-field or per-operation instrumentation you must add.
Straightforward status codes. GraphQL returns 200 with errors in the response body. Monitoring based on HTTP status codes sees success while clients receive errors, which means alerting must be rebuilt around response content.
Trivial file uploads. Not part of the specification. Handled by convention or by a separate REST endpoint.
Low operational complexity. A schema, resolvers, batching layer, depth limits, cost analysis, and persisted query infrastructure is meaningfully more machinery than a set of HTTP handlers.
None of these are insurmountable. All of them are work, and the total is frequently underestimated because each individual item seems small.
The N+1 Problem Is Structural
This is the failure mode that makes naive GraphQL implementations perform badly, and it follows from how resolvers work rather than from a mistake.
Consider a query fetching fifty posts, each with its author:
{
posts(first: 50) {
title
author { name }
}
}
The posts resolver runs once and returns fifty posts. The author resolver then runs once per post — fifty separate database queries for authors, many of them the same author.
One query for posts plus N queries for authors. Nest another level and it multiplies.
This is structural because resolvers are independent functions with no knowledge of their siblings. The author resolver receives one post and cannot know that forty-nine others are being resolved concurrently.
The standard solution is a batching layer that collects resolver calls within a tick, deduplicates the keys, and issues one query for all of them.
Without batching: SELECT * FROM users WHERE id = 12 (×50)
With batching: SELECT * FROM users WHERE id IN (12, 47, 3, ...)
This works well and it is not optional — it is required infrastructure for any GraphQL server touching a database. Every resolver that loads related data needs it, and a resolver added without it introduces an N+1 that may not be noticed until production traffic reveals it.
The practical consequence: batching discipline must be part of code review, because the failure is invisible in development with small datasets.
Query Cost Is Unbounded by Default
A GraphQL schema permits any query the type system allows, including ones that are extremely expensive.
# Depth: each level multiplies the work
{ user { posts { comments { author { posts { comments { ... } } } } } } }
With circular relationships in the schema — a user has posts, a post has an author — arbitrarily deep queries are expressible. A single request can consume enormous server resources, which is a denial of service vector requiring no special access.
The controls required, all of which must be added deliberately:
Depth limiting. Reject queries nested beyond a maximum. Simple and effective as a first line.
Complexity analysis. Assign a cost to each field, sum the query’s cost, and reject beyond a budget. More precise than depth alone, since a shallow query requesting thousands of items is also expensive.
Pagination requirements. Enforce that list fields require a limit, with a maximum. Unbounded lists are the most common source of accidental expense.
Timeouts per resolver and per query. Bounding worst-case duration regardless of what was requested.
Persisted queries. Clients register queries in advance and reference them by identifier at runtime. The server executes only known queries, which eliminates the entire class of problem and also enables HTTP caching by making requests GET-able with a stable key.
Persisted queries are the strongest control available and they remove GraphQL’s flexibility for third parties, which is why they suit internal clients well and public APIs poorly. For a public GraphQL API, complexity analysis with a per-client budget is the realistic approach.
Authorisation Moves to Every Field
In REST, authorisation attaches to endpoints. One check per handler, and the set of handlers is enumerable.
In GraphQL, clients traverse the graph freely, so authorisation must be enforced wherever data is reached. The same field may be reached through many paths, and a check present on one path is not present on the others.
# All three reach user email — every path needs the check
{ user(id: 5) { email } }
{ post(id: 9) { author { email } } }
{ organisation(id: 2) { members { email } } }
This means field-level authorisation rather than endpoint-level, and it is easier to leave a gap because the paths are not enumerable by reading a route table.
Approaches that work: implement authorisation in the data-access layer rather than in resolvers, so every path goes through it regardless of how the field was reached. Use schema directives to declare requirements declaratively, which makes them visible in the schema. Scope queries by the authenticated principal at the source, so unauthorised data is unreachable rather than filtered.
And introspection deserves a decision. A public schema reveals the entire data model, which is useful for developer experience and informative to anyone probing for weaknesses. Disabling introspection in production is common for internal APIs and counterproductive for public ones.
Error Handling Is Genuinely Different
GraphQL’s error model surprises teams arriving from REST, and the differences affect monitoring.
A GraphQL response typically returns HTTP 200 even when resolution failed, with errors listed in an errors array alongside whatever data was successfully resolved.
Partial success is normal: some fields resolve, others error, and the client receives both. This is a genuine feature — a failure in one part of a query need not fail the whole thing — and it means several assumptions need revisiting.
Consequences to plan for. Monitoring keyed to HTTP status codes reports success while clients receive errors, so alerting must inspect response bodies. Clients must handle a response containing both data and errors, which many client implementations do awkwardly. And errors need structured codes in their extensions, because a human-readable message is not something a client can branch on reliably.
There is also a security consideration: resolver errors can leak implementation detail — database messages, internal service names, stack traces. Error responses need sanitising before they reach clients, with the full detail logged server-side.
When Each Choice Fits
| Situation | Better choice |
|---|---|
| Several client platforms with diverging data needs | GraphQL |
| One web client you control | REST |
| Mobile clients that cannot deploy quickly | GraphQL |
| Public API for many third parties | REST, usually |
| Deeply relational data with variable traversal | GraphQL |
| Simple resource CRUD | REST |
| Heavy reliance on CDN caching | REST |
| Need for predictable, bounded per-request cost | REST |
| File upload as a core feature | REST |
| Aggregating several backend services for clients | GraphQL |
| Small team, limited operational capacity | REST |
The pattern: GraphQL earns its complexity when client diversity is high and data is relational. It does not when there is one client, when caching matters more than flexibility, or when the team lacks capacity for the additional infrastructure.
A common and effective arrangement is both. GraphQL as an aggregation layer for first-party clients, REST for public consumers and for operations that do not fit the model — file uploads, webhooks, bulk operations. These are not in competition, and treating the choice as exclusive is unnecessary.
Getting Most of the Benefit From REST
Several of GraphQL’s advantages are achievable in REST with modest effort, which is worth knowing before adopting a new stack.
Sparse fieldsets. A fields parameter letting clients specify what they need addresses over-fetching directly.
GET /users/42?fields=id,name,avatar_url
Compound documents. An include parameter embedding related resources in one response addresses the waterfall problem.
GET /posts/9?include=author,comments.author
Composite endpoints for specific views. An endpoint returning exactly what one screen needs. Less general than GraphQL and much simpler, and entirely appropriate when the number of screens is bounded.
Cursor pagination with sensible defaults. Bounded response sizes without client cooperation.
Conditional requests. ETags and validation headers reduce transferred bytes for unchanged resources, which GraphQL cannot easily do.
The first two cover a large share of what motivates GraphQL adoption. If over-fetching and waterfalls are the actual problems, they are addressable without changing your entire API architecture — and the result retains HTTP caching, predictable cost, and status-code-based monitoring.
Common Pitfalls
No batching layer. Guarantees N+1 queries and poor performance.
No depth or complexity limits. Leaves a denial of service vector open to any client.
Endpoint-style authorisation. Fields reachable by several paths need checks on all of them.
Assuming HTTP caching works. It largely does not without persisted queries.
Monitoring HTTP status codes only. Reports success while clients receive errors.
Adopting GraphQL for one client. Substantial complexity for a problem that does not exist.
Exposing the database schema as the graph. Couples the API to storage and produces a poor client-facing model.
Conclusion
GraphQL solves over-fetching, request waterfalls, endpoint proliferation, and client-server release coupling. Those are real problems and the solution is genuinely good where they are painful — several client platforms, relational data, mobile clients that cannot ship quickly.
The costs are mostly infrastructure that REST gives you free: HTTP caching becomes an application concern, per-request cost becomes unbounded and requires depth and complexity limits, authorisation moves from endpoints to fields where gaps are easier to leave, and monitoring must be rebuilt around response bodies rather than status codes. Plus a batching layer, which is mandatory rather than optional.
Before adopting it, check whether you have the problem. One client and endpoints that return roughly the right shape means the problems GraphQL solves are not the ones you have. Sparse fieldsets and an include parameter address over-fetching and waterfalls in REST for a fraction of the effort.
And where GraphQL does fit, using both is normal — GraphQL for first-party client aggregation, REST for public consumers, uploads, and bulk operations.
Frequently Asked Questions
Is GraphQL faster than REST? Sometimes, by eliminating request waterfalls and over-fetching. Frequently slower per request due to resolver overhead and lost HTTP caching. The comparison depends entirely on the access pattern.
How is the N+1 problem avoided? A batching layer that collects resolver calls within a tick and issues one query per batch. This is required infrastructure, and every relational field needs it.
Can GraphQL responses be cached? Not with standard HTTP caching for POST queries. Persisted queries with GET requests enable it. Otherwise caching must be implemented in the application, at the resolver or data-loader level.
Should introspection be disabled in production? Common for internal APIs, where it removes a reconnaissance aid. Counterproductive for public APIs, where the schema is documentation. It is not a security control by itself.
How is authorisation handled properly? In the data-access layer rather than in resolvers, so every path to a field goes through the check. Schema directives make requirements visible; enforcement belongs where data is loaded.
Can REST and GraphQL coexist? Yes, and it is a common arrangement. GraphQL for first-party client aggregation, REST for public consumers, file uploads, and bulk operations.
What is the largest hidden cost? Losing HTTP caching. Teams accustomed to CDN caching by URL discover that they must build an equivalent, which is substantially more work than the header it replaces.



