Vivian Voss

GraphQL: The Query You Didn’t Need

graphql rest api performance

Performance-Fresser ■ Episode 15

The Origin Story

In 2012, three engineers at Facebook — Nick Schrock, Dan Schafer, and Lee Byron — had a genuine problem. The News Feed served hundreds of content types across three platforms, each requiring different data shapes. Billions of API calls per day. REST endpoints were multiplying faster than the team could maintain them. So they built a query language that let each client ask for precisely the fields it needed.

Facebook reported 67 per cent less bandwidth on mobile. Shopify measured a 41 per cent data reduction. At that scale, with that problem, GraphQL is genuinely the right tool.

You have 12 endpoints. Terribly sorry.

The Promise

“Ask for exactly what you need.” No over-fetching, no under-fetching. One request, precisely shaped. The pitch is elegant, the schema is typed, and the developer experience on day one is genuinely pleasant. Everyone on the conference circuit nodded.

Then the invoices arrived.

Request Processing: REST vs GraphQL REST Request Route + SQL Response 2 steps GraphQL Request Parse query Validate schema Resolve chain Response 5 steps 50% REST faster on relational DBs (Lawi et al., 2021) 34x REST eager loading vs GraphQL + DataLoader (FreeCodeCamp)

The Tax

Every GraphQL request pays an entry fee that REST does not charge. The query string must be parsed. The schema must be validated. The resolver chain must be walked. REST routes directly to a handler and executes one query. The overhead is not theoretical.

A 2021 peer-reviewed study by Lawi et al. found REST up to 50 per cent faster than GraphQL on relational databases. A FreeCodeCamp benchmark measured REST with eager loading at 34 times faster than GraphQL with DataLoader — on 1,000 posts with 50 comments each.

The industry's response: adopt both. The Postman State of API 2025 report (5,700 respondents) found 93 per cent of developers use REST. 33 per cent use GraphQL, mostly in addition. RapidAPI's 2024 data shows 83 per cent of public APIs use REST. The industry adopted a solution and kept the problem.

The N+1 Inheritance

GraphQL resolvers fire per field. Query 10 posts with their comments: one query for the posts, then one query per post for the comments. Eleven queries where REST with a SQL JOIN does it in one.

The N+1 Problem: 10 Posts With Comments GraphQL SELECT * FROM posts SELECT comments WHERE post=1 SELECT comments WHERE post=2 SELECT comments WHERE post=3 SELECT comments WHERE post=10 11 queries REST + JOIN SELECT p.*, c.* FROM posts p JOIN comments c 1 query DataLoader batches the 10 into 2. Quite clever. REST’s default is still 1. The mitigation is still slower than the behaviour it mitigates.

DataLoader batches those 10 into 2. Quite clever. But REST with a SQL JOIN still does it in one. The mitigation is architecturally sound and measurably slower than REST’s default behaviour. You are adding a caching layer to restore what the query layer removed.

The Caching Void

REST uses GET requests. GET requests are cacheable by definition. CDNs cache GET responses at the edge. Fastly reports 78 per cent average cache hit rates for REST APIs.

GraphQL uses POST by default. CDNs cache GET, not POST. Your query is in the request body, invisible to the cache layer. The GraphQL specification permits GET, but the ecosystem defaults to POST. Apollo’s Automatic Persisted Queries work around this by hashing queries into GET parameters — an additional layer to restore what REST had natively. The Apollo team themselves called caching “the elephant in the room.”

HTTP Caching: GET vs POST REST GET /api/posts CDN Edge cache: HIT Response 78% hit rate GraphQL POST /graphql CDN Edge cache: SKIP Origin every time 0% hit rate Apollo Persisted Queries: hash queries into GET parameters. An additional layer to restore what REST had natively.

The 200 Problem

REST uses HTTP status codes. 200 means success. 404 means not found. 500 means the server is having a bad day. Your monitoring, your CDN routing, your alerting: built on these codes. They work.

GraphQL returns 200 for everything. Success, partial failure, catastrophe: all 200. Errors live in the response body, in a JSON field your monitoring has to learn to read. Your existing infrastructure — load balancers, CDN rules, Grafana dashboards, PagerDuty triggers — all built on status codes. All blind.

Fixable, yes. You add software to solve what the software introduced.

The Client Tax

The browser ships the Fetch API. Built-in. Zero kilobytes. A REST call is one function invocation:

const data = await fetch('/api/posts').then(r => r.json());

Apollo Client: 137 KB minified. Plus the graphql dependency. Plus code generation. Plus schema management. Plus query complexity analysis. You added an API query language to avoid writing a second endpoint.

Client Bundle: What You Ship Before Your Code Runs fetch() 0 KB (built-in) Apollo Client 137 KB min + graphql dependency + codegen tooling + schema management You added an API query language to avoid writing a second endpoint.

The Security Surface

REST exposes a finite set of endpoints. You can enumerate them, audit them, rate-limit them individually. Each endpoint returns a predictable shape.

GraphQL exposes a single endpoint with an infinite query space. Imperva analysed 6,000 GraphQL endpoints and found 50 per cent had been targeted with introspection attacks. CVE-2022-37734 demonstrated denial-of-service via directive overloading. There are no default depth limits. No default complexity limits. Every guard rail is opt-in, and every opt-in is a line of code somebody has to remember to write.

The Adoption Arithmetic

API Adoption (Postman 2025, 5,700 respondents) REST 93% GraphQL 33% gRPC 11% 83% of public APIs use REST (RapidAPI 2024). GraphQL is mostly used in addition, not as replacement.

The Admission

I have used GraphQL professionally for years. The schema was elegant. The developer experience was pleasant. The tooling was polished. But none of the projects required it. REST would have served every one of them with less overhead, better caching, and simpler monitoring.

The honest question is not “Does GraphQL work?” It does. The question is: “Does your problem require it?”

If you are Facebook, yes. If you serve multiple clients with genuinely different data needs across platforms, perhaps. If you have one backend serving one frontend: you are paying the tax for someone else’s problem.

GraphQL was built for hundreds of content types across three platforms. You have 12 endpoints. The query language is not solving your problem. It is the problem you are solving.