REST vs GraphQL: An Honest Comparison With Our Own Measurements (2026)

REST vs GraphQL: An Honest Comparison With Our Own Measurements (2026)

If you build or integrate an API, sooner or later you hit this question: REST vs GraphQL, which one is better? The internet has two camps. One says REST is outdated and GraphQL is the future. The other says GraphQL is hype nobody outside Facebook needs. Both are wrong, and they’re wrong in interesting ways.

The short answer: REST and GraphQL are two ways of designing web APIs. With REST there are many addresses (one per resource), and the server decides what goes into the response. With GraphQL there’s a single address, and the client describes in a query language which fields it wants. Almost everything else follows from that: GraphQL saves requests and bytes, REST is easier to cache, secure and operate.

So this article doesn’t just repeat opinions, on October 13, 2026 we measured two things: the same task against the real GitHub API, which offers both styles, and our own test server serving identical data over REST and GraphQL. Three results up front:

1. One repository, plus its last 10 pull requests, plus each author’s name and follower count: REST needed 12 requests, 248 KB and about 3.5 seconds. GraphQL needed 1 request, 1.3 KB and about 0.4 seconds.

2. On our own server, a simple REST endpoint handled 44,671 requests per second; the equivalent GraphQL query handled 6,075. GraphQL costs more per request because every query has to be parsed and validated.

3. A 186-byte GraphQL query made our unprotected test server build a 13 MB response and perform 156,222 data lookups to do it. REST doesn’t have this problem in this form.

On the left a row of many small glowing doors each with an envelope in front, on the right one large round portal where an envelope flows in and a neatly shaped bundle comes out

REST vs GraphQL: The Fundamental Difference

If you’re not quite sure what an interface is in the first place, start with our primer What Is an API?. This article is about how you build one.

How REST works

REST (Representational State Transfer) isn’t a protocol or a piece of software. It’s an architectural style Roy Fielding described in his 2000 dissertation. In practice, “REST API” today usually means:

  • Every resource has its own URL: /repos/withastro/astro, /users/octocat, /repos/withastro/astro/pulls.
  • What you want to do is expressed by the HTTP method: GET reads, POST creates, PUT/PATCH updates, DELETE deletes.
  • Whether it worked is in the status code: 200, 201, 404, 401, 429 and so on.
  • The response is usually JSON, and the server decides which fields are in it.

A REST call looks like this:

curl https://api.github.com/repos/withastro/astro

You get back a JSON object with over a hundred fields: name, description, stars, license, owner, dozens of URLs to other resources. Whether you need them or not.

How GraphQL works

GraphQL was developed at Facebook from 2012 onward, because the mobile app needed numerous REST requests over slow cellular networks to render a single screen (the news feed). It was open-sourced in 2015, and since 2018 it’s been stewarded by the GraphQL Foundation under the Linux Foundation.

The idea: there’s one URL, usually /graphql. The client sends a query via POST describing exactly what data it wants, and gets a response in exactly that shape:

{
  repository(owner: "withastro", name: "astro") {
    name
    stargazerCount
  }
}

Response:

{ "data": { "repository": { "name": "astro", "stargazerCount": 63194 } } }

Two fields asked for, two fields received. Behind the query language there’s a schema, a strictly typed blueprint of every type and field the API knows. The server validates each query against this schema before running it.

The differences in one table

RESTGraphQL
Addressesmany, one per resourceusually one (/graphql)
Who picks the fields?the serverthe client
HTTP methodsGET, POST, PUT, PATCH, DELETEalmost always POST
Errorsvia status codes (404, 401 …)often status 200 with an errors field
HTTP cachingbuilt in (ETag, Cache-Control, CDN)hard, usually done in the client
Schemaoptional (OpenAPI)mandatory, built in
Versioningoften /v1/, /v2/deprecate fields instead
Real timeseparate (WebSockets, SSE)subscriptions in the spec
Getting startedvery easy, one curl is enoughquery language, schema, resolvers
Main risktoo many requests, too much dataexpensive queries, N+1, abuse

Our Test Setup

We measured in two places so the result isn’t just an artificial lab number.

Test 1: the real GitHub API. GitHub is one of the few big platforms that serves the same data over REST (API v3) and GraphQL (API v4). The task: the repository withastro/astro (which, incidentally, is what getmind.io runs on), its last 10 closed pull requests, and each author’s name and follower count. Queried with a personal token from our server in Nuremberg, three runs in a row.

Test 2: our own server. A small Node.js server (Node 22.23, graphql 17.0.2, graphql-http 1.23.1) with 100 authors, 2,000 posts and 10,000 comments in memory. The same data is reachable via REST endpoints (/api/posts, /api/authors/:id) and via GraphQL, once with naive resolvers and once with batching (more on that below). Load testing with autocannon 8.0.0: 16 concurrent connections, 10 seconds each, on a 12-core VPS, everything on the same machine.

Important context: Test 2 measures the overhead of the technology, not the speed of a real database. The data sits in RAM and the network is localhost. In real applications the database is almost always the slowest part, and the differences shrink accordingly.

Test 1: How Many Requests Does One Page Need?

Picture an overview page: repository name, star count, the last ten pull requests, and next to each one the author’s full name and follower count. Pretty ordinary.

With REST it goes like this:

  1. GET /repos/withastro/astro → name and stars
  2. GET /repos/withastro/astro/pulls?state=closed&per_page=10 → ten pull requests, but only the author’s login
  3. through 12. GET /users/<login> → for each of the ten authors, one request for full name and followers

With GraphQL it’s a single query:

{
  repository(owner: "withastro", name: "astro") {
    name
    stargazerCount
    pullRequests(states: [CLOSED, MERGED], last: 10) {
      nodes {
        title
        number
        author {
          login
          ... on User { name followers { totalCount } }
        }
      }
    }
  }
  rateLimit { cost remaining }
}

Results across three runs:

RESTGraphQL
HTTP requests121
Response size (uncompressed)247,806 bytes1,324 bytes
Total time3,418 – 3,847 ms410 – 442 ms
Rate limit used12 of 5,000 per hour1 of 5,000 points per hour

Twelve small glowing orbs travel a looping path between two pillars, while a single larger orb makes one short straight trip

The number that jumps out is size: a factor of 187. Part of that is cosmetic, since we counted uncompressed and JSON compresses well. But most of it is genuine over-fetching: every REST response contains dozens of fields nobody asked for; the repository object alone is mostly URLs to other resources.

The time difference, on the other hand, comes almost entirely from round trips. Our script sent the requests one after another, as many simple frontends do, and each round trip to GitHub costs about 300 milliseconds. With parallel requests you could squeeze REST down to roughly three round trips (repo and pull requests at once, then all ten authors at once), but not to one, because you don’t know the authors until the pull request list arrives. That under-fetching, where one response isn’t enough and you have to fetch more, is exactly why Facebook invented GraphQL.

For a phone app on a train, that’s not a theoretical difference. Twelve round trips over a network with 150 ms latency is nearly two seconds of waiting before anything renders.

Over-fetching and under-fetching, simply explained

These two terms show up in every REST vs GraphQL comparison:

  • Over-fetching: you get more data than you need. You want a name and receive an object with 100 fields.
  • Under-fetching: one response isn’t enough, so you send follow-up requests. You get pull requests, but only IDs for the authors.

REST can soften both, with parameters like ?fields=name,stars or ?include=author, and many big APIs offer that. But then it’s a custom solution per API, whereas GraphQL has it as a core principle.

A large cube overflowing with dozens of small spheres spilling onto the floor, next to a small cube neatly holding exactly three spheres

Test 2: How Fast Is a Single Call?

Now the other side. On our own server we measured how many requests per second each variant can handle:

CallRequests per secondp99 latency
REST GET /api/authors/5 (one record)44,671< 1 ms
GraphQL { post(id: 5) { title } } (one record)6,0755 ms
REST GET /api/posts?limit=20 (20 posts, 37 KB)16,3722 ms
GraphQL 20 posts with author, naive5,0456 ms
GraphQL 20 posts with author, batched4,0149 ms

For a single record, REST was a good seven times faster on our server. That’s not because GraphQL is badly implemented; it’s the extra work per request: parse the query text, validate it against the schema, call a resolver for every single field, and shape the response into the requested form. REST just hands out a finished object via JSON.stringify.

The last row stands out too: with batching, GraphQL was slower than naive on our test server. That sounds paradoxical but isn’t. Our “database” is an in-memory array; a lookup costs practically nothing. Batching (collect, promise, distribute) does cost something. With a real database, where each lookup is a network round trip, the picture flips immediately. That’s the next section.

Perspective: 6,000 requests per second on one process is more than enough for almost any website. Our whole blog gets fewer visits in a day than this server handles in a second. GraphQL’s overhead is real, but for most projects it isn’t the bottleneck.

The N+1 Problem: GraphQL’s Best-Known Trap

Search “GraphQL slow” and you almost always land here. The problem comes from how GraphQL servers execute queries: every field has its own resolver, a small function that fetches exactly that field. For the query

{ posts(limit: 20) { title author { name } } }

the naive execution goes like this:

  1. One resolver fetches the 20 posts → 1 database query
  2. For each of the 20 posts, the author resolver fetches the author → 20 database queries

That’s 21 queries for one list. Hence the name: 1 for the list, plus N for each item. That’s exactly what we counted on our test server:

VariantData lookups for 20 posts with author
GraphQL, naive resolvers21
GraphQL with batching (DataLoader pattern)2

The standard fix is called DataLoader, also originally from Facebook. Instead of hitting the database immediately, the resolver collects all requested IDs until the end of the current tick and then sends one WHERE id IN (…) query. Our test server does this in about ten lines of code; real projects use the ready-made dataloader library.

Important: N+1 is not purely a GraphQL problem. Any ORM or template that lazy-loads inside a loop can trigger it, in a REST API too. The difference is that with REST, the server developer knows the queries behind an endpoint and can optimize them once. With GraphQL, any client can send new combinations nobody thought about at build time.

The 186-Byte Query: Where GraphQL Gets Dangerous

Now it gets serious. In our schema, a post points to its comments, and each comment points back to its post. That’s completely normal and true of almost every real data model. But it allows queries like this:

{
  posts(limit: 20) {
    comments {
      post { comments {
        post { comments {
          post { comments { text } }
        } }
      } }
    }
  }
}

Every extra level multiplies the work. We sent this step by step against our server with no protection in place (batching for authors was on, but didn’t help here because comments are loaded per post):

Nesting depthQuery sizeResponse sizeData lookupsTime
178 bytes19,920 bytes22120 ms
2105 bytes103,420 bytes1,22153 ms
3132 bytes520,920 bytes6,221185 ms
4159 bytes2,608,420 bytes31,221270 ms
5186 bytes13,045,920 bytes156,2221,269 ms

Each level, times five. A request shorter than this paragraph kept our server busy for over a second and produced 13 MB of response. A sixth level would have landed around 65 MB and 780,000 lookups; we skipped that one. Ten of these per second are enough to take down a small server, and you don’t need a botnet for that, just one laptop.

A small pebble drops into a calm pond and sets off an enormous branching wave of glowing limbs

REST doesn’t have this problem in this form: an endpoint always returns the same server-defined amount. To attack a REST API you have to send many requests, and any rate limiter spots many requests immediately.

How to protect a GraphQL API

The problem is well known and solvable, just not solved automatically. What helps in practice:

  1. Depth limiting: reject queries deeper than, say, 7 levels (libraries such as graphql-depth-limit).
  2. Cost analysis: give every field a cost, multiply lists by their maximum length, and reject expensive queries before running them. GitHub does exactly this: our query above cost 1 point out of 5,000 per hour according to the response. Ask for lists of 100 inside lists of 100 and you pay accordingly.
  3. Mandatory list limits: no list field without first/last, with a cap (GitHub: max 100).
  4. Persisted queries: in your own app, the queries are known at build time. Store them on the server and allow only those; the client just sends an ID. For internal APIs that pretty much settles it.
  5. Timeouts on the execution of every query.

Introspection: the API describes itself

GraphQL has a built-in feature that lets any client fetch the complete schema:

{ __schema { types { name } } }

Our test server happily returned every type, field and relationship. For developers that’s great: tools like GraphiQL use it for autocomplete and documentation without anyone writing a line of docs. For attackers it’s a floor plan. That’s why many operators turn introspection off in production when the API isn’t meant to be public. It still isn’t real protection, since error messages give hints too. For { post(id: 5) { titel } } our server replied: “Cannot query field “titel” on type “Post”. Did you mean “title”?”

We see attackers actively hunting for GraphQL endpoints in our own logs. Our blog has no API at all, yet between September 7 and October 12, 2026 it received 309 requests to /graphql, 174 to /api/graphql and 167 to /v1/graphql. Details are in the API article.

Caching: REST’s Biggest Practical Advantage

REST uses HTTP the way it was meant to be used. A GET on a URL is a read, and the whole internet (browsers, proxies, CDNs, reverse proxies) knows how to cache that. We tried it on GitHub:

  • REST: the response to GET /repos/withastro/astro came with an ETag and Cache-Control: private, max-age=60, s-maxage=60. The second call with If-None-Match returned 304 Not Modified, meaning no data, just confirmation that nothing changed.
  • GraphQL: the response to a POST query to /graphql had neither ETag nor Cache-Control. No cache in the world caches POST requests on its own.

So with GraphQL, caching moves into the client. Libraries like Apollo Client or urql store results normalized by type and ID and know which components to re-render. That works well, but it’s far more logic than setting a header. Server-side, there are solutions using GET queries with persisted queries (our test library accepted GET /graphql?query=… just fine with a 200), but that’s extra work you have to build in deliberately.

For a public API with lots of anonymous readers, this is often the deciding factor: a REST response a CDN serves for one minute costs your server zero load during that minute.

Error Handling: 404 or 200?

We requested the same non-existent resource from GitHub both ways:

  • REST: GET /repos/withastro/nope-xyz → HTTP 404.
  • GraphQL: repository(owner: "withastro", name: "nope-xyz") → HTTP 200, with "repository": null in the body and an errors array containing "type": "NOT_FOUND".

That isn’t a GitHub bug; it’s by design. A GraphQL query can ask for ten things at once, nine of which succeed, and a single status code can’t express that. For you as a developer it means: monitoring that only looks at status codes sees no errors with GraphQL. Your dashboard shows 100% green 200s while half the responses contain errors. You have to inspect the body.

Newer servers now follow the “GraphQL over HTTP” draft: if you request the media type application/graphql-response+json, invalid queries get a real status code. Our test server then answered the typo query with 400 Bad Request instead of 200. You can’t rely on that standard with every API yet, though.

Rate Limits: Count Requests or Count Cost?

With REST, a rate limit is simple: so many requests per hour. GitHub allows 5,000 with a token. Our REST run used 12 of them each time (the quota dropped from 5,000 to 4,988, 4,976, 4,964).

With GraphQL that doesn’t work, because a single request can be harmless or devastating. So GitHub counts points: a query’s cost is estimated before execution, and the response tells you what it cost (rateLimit { cost remaining }). Our query: 1 point. The catch: every GraphQL provider has to build such cost models themselves, and for users they’re harder to predict than “one request = one”.

A side detail that surprised us: without a token, GitHub’s GraphQL API doesn’t answer at all; it returns 403 with x-ratelimit-limit: 0. The REST API at least allows 60 anonymous requests per hour. If you want to try something quickly, REST gets you further without signing in.

Versioning and Evolution

REST APIs are often versioned in the URL: /v1/users, /v2/users. Simple and unambiguous, but it means running several versions side by side until the last old client disappears. Many APIs now use date-based versions in a header instead.

GraphQL deliberately avoids versions. You can add new fields anytime because no existing client asks for them. Old fields get marked @deprecated, and you check the logs for who still uses them. Since every client states exactly which fields it reads, the operator knows precisely what’s safe to remove. That’s a real advantage when many different apps share the same API.

Developer Experience: What Feels Different Day to Day

In favor of REST:

  • Anyone can try it immediately: browser, curl, Postman, done.
  • Every language and framework has it built in. A REST endpoint in Express, Laravel or Django is a handful of lines.
  • Logs, monitoring, firewalls and rate limiters understand URLs and status codes out of the box.
  • File uploads and downloads are easy because HTTP was made for them. GraphQL needs extensions for that.

In favor of GraphQL:

  • The schema is the documentation, and it’s always current.
  • With code generators you automatically get TypeScript types for every query from the schema. Typos in field names surface at build time, not in front of a customer.
  • Frontend teams can build new views without waiting on new endpoints from the backend.
  • Several sources (database, microservices, third-party APIs) can be unified behind one interface.

Also in favor of REST: OpenAPI (formerly Swagger) now largely closes the schema gap. You can generate types and clients from it too. The gap is smaller than many GraphQL articles claim.

From May 2026: a practical, beginner-friendly walkthrough of both styles. Note that it leans toward GraphQL; the caching and security trade-offs above are worth keeping in mind while watching.

What About gRPC? The Third Option

Comparisons often include a third contender: gRPC, originally from Google. Here data isn’t sent as JSON text but in binary (Protocol Buffers) over HTTP/2, and client and server share a strictly defined interface description. It’s very fast and compact, but only usable in the browser through workarounds.

A rough rule of thumb: REST for public and simple APIs, GraphQL for frontends with complex data needs, gRPC for service-to-service communication inside your own infrastructure. Many large companies use all three at once.

May 2026: an overview of REST, GraphQL and gRPC with practical decision criteria.

Who Uses What? A Look at Well-Known Systems

  • GitHub offers both REST and GraphQL. New features often land in GraphQL first.
  • Shopify made GraphQL its preferred interface; the REST Admin API is considered legacy there.
  • Shopware relies on REST (Store API and Admin API) with its own search format that lets you select fields and associations. A good example of REST partly rebuilding GraphQL’s advantages. More on the platform in Shopify vs Shopware.
  • Magento/Adobe Commerce has both REST and GraphQL; the storefront side increasingly runs on GraphQL. Magento’s CMS content is available over GraphQL too, which we looked at in detail in Magento CMS.
  • Stripe, Twilio and nearly every payment and communications service use REST, because thousands of different customers integrate them from every language.
  • WordPress ships a REST API; GraphQL is available via the WPGraphQL plugin, especially popular for headless setups.

An honest note about our own day-to-day: the services on our main server speak almost exclusively REST. For a handful of endpoints, one known frontend and manageable data models, GraphQL would have been more effort than benefit. Where we do use GraphQL, it’s almost always because an third-party system (like GitHub) offers it and we need lots of linked data at once.

When to Use REST, When to Use GraphQL

A person stands at a fork in the road; one path leads to a row of simple tidy mailboxes, the other to a glowing hub with many cables

Choose REST if …

  • the API is public and used by many different customers,
  • many readers request the same data and HTTP caching or a CDN can take most of the load,
  • your data models are manageable and pages don’t need ten resources at once,
  • you transfer files,
  • your team is small and operations, monitoring and security should stay simple,
  • you’re just starting out building APIs.

Choose GraphQL if …

  • several frontends (web, iOS, Android) need the same data in different shapes,
  • pages assemble data from many linked resources,
  • mobile users arrive over slow networks and every round trip counts,
  • frontend and backend teams should work independently,
  • you want to bundle several services behind one interface,
  • you’re willing to set up depth limits, cost analysis and batching properly.

You can do both. A very common setup: REST third-partyly for partners and integrations, GraphQL internally as the data layer for your own apps. Or a “backend for frontend” that queries several REST services internally and gives the frontend exactly what it needs.

Checklist: REST or GraphQL in Six Questions

  1. Who uses the API? Third-party developers in many languages → lean REST. Your own apps → GraphQL is more likely worth it.
  2. How many resources does a typical view need? One or two → REST. Five or more, nested → GraphQL.
  3. Can responses be cached? Yes, the same for many users → big plus for REST.
  4. How slow are your users’ networks? Mobile, emerging markets → GraphQL saves noticeable time.
  5. Who runs the system? A small team without time for hardening → REST. A team that will set up cost limits and monitoring → GraphQL is doable.
  6. What tooling do you already have? If your framework or shop system supports one of them well, use that one.

Frequently Asked Questions About REST vs GraphQL

What is the difference between REST and GraphQL?

REST exposes many addresses, one per resource, and the server decides what data comes back. GraphQL usually exposes a single address, and the client uses a query language to describe exactly which fields it wants. That means GraphQL needs fewer requests and transfers less unnecessary data, while REST is easier to cache and secure.

Is GraphQL faster than REST?

It depends on what you measure. For a page that needs lots of linked data, GraphQL was about eight times faster in our GitHub test (1 request instead of 12). For a single record, REST was about seven times faster on our own server, because GraphQL has to parse and validate every query. GraphQL saves round trips; REST saves compute per request.

Is GraphQL better than REST?

No, it’s different. GraphQL solves over- and under-fetching and is excellent for complex frontends. In exchange, caching, rate limiting, error handling and security take more work. For public, simple or heavily cached APIs, REST is usually the better choice.

Is GraphQL replacing REST?

No. According to Postman’s 2025 developer survey (as cited by WunderGraph), the vast majority of developers still use REST, while GraphQL is used by roughly a third, mostly alongside it. In practice both run side by side, often within the same company.

Is GraphQL a database?

No, even though the name suggests it (“QL” as in SQL). GraphQL is a query language for APIs. Where the data comes from is up to the resolvers on the server: a database, other APIs, files, or all of them at once.

Can you use GraphQL and REST together?

Yes, and it’s very common. Many companies expose a REST API third-partyly and use GraphQL internally as the data layer for their own apps. A GraphQL server can also call existing REST services itself and merge their data.

What is the N+1 problem in GraphQL?

When you query a list of 20 items, each with a linked object, naive resolvers first fetch the list (1 query) and then each object individually (20 queries). In our test that was 21 data lookups instead of 2. The fix is batching, for example with the DataLoader library, which collects all IDs and fetches them in one query.

Is GraphQL less secure than REST?

Not inherently, but it has its own risks. Because clients can build arbitrary queries, a short, deeply nested query can create enormous load. In our test, 186 bytes produced a 13 MB response and over 156,000 data lookups. Depth limits, cost analysis, list limits and persisted queries make it manageable, but you have to set them up actively.

Can GraphQL responses be cached?

Yes, but not as easily as with REST. Because GraphQL almost always uses POST, browser caches and CDNs don’t kick in by themselves. Caching usually happens in the client (Apollo, urql) or on the server via persisted queries over GET. With REST, a few HTTP headers are often enough.

What HTTP status codes does GraphQL use?

Traditionally a GraphQL server answers with 200 almost always, even on errors, and puts the errors in an errors field in the JSON. Newer servers following the “GraphQL over HTTP” draft return 400 and other fitting codes for invalid queries when the client requests the application/graphql-response+json type.

Should a beginner learn REST or GraphQL first?

REST. It builds directly on HTTP, you can follow every step with a browser or curl, and almost every API you’ll touch uses it. Once you understand REST, you’ll understand much faster which problems GraphQL solves. Our article What Is an API? is a good starting point, and for the data format behind it, What Is a JSON File?.

Conclusion

“REST vs GraphQL” has no universal answer, but it has a very good answer for your case if you measure the right things. Our measurements show the contrast clearly: GraphQL fetched a nested view in 1 request instead of 12, with 1.3 KB instead of 248 KB, in an eighth of the time. In return, REST was about seven times faster per individual request, can be cached with a header, reports errors via status codes, and doesn’t know the 186-byte query that keeps an unprotected GraphQL server busy for over a second.

Our recommendation: start with REST unless you have a concrete reason for GraphQL. That reason is usually one of three: several apps with different data needs, views built from many linked resources, or users on slow connections. If one of those applies, GraphQL is an excellent choice. But then with depth limits, cost analysis and DataLoader from day one, not after the first outage.