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.
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.
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.”
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.
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
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.