Chat on WhatsApp
Headless & Architecture 12 min read

Magento API Performance — REST vs GraphQL Trade-offs Explained With Real Numbers

Most Magento REST vs GraphQL articles compare syntax. After benchmarking three real queries against a Magento 2.4.9 store with 50,000 SKUs, the answer flips depending on the call pattern: REST wins single-resource by URL (95 ms vs 140 ms), GraphQL wins relational and grid loads by an order of magnitude (180 ms vs 665 ms), and the FPC versus @cache directive story decides whether either survives Black Friday. Here is the curl trace, the wrk concurrency test, and the cache invalidation pattern from client work on Magento 2.4.4 — 2.4.9.

Magento API Performance — REST vs GraphQL Trade-offs Explained With Real Numbers

Magento API performance is the engineering trade-off between Magento's REST V1 endpoints and the GraphQL endpoint exposed at /graphql on Magento 2.4.4 — 2.4.9 — REST is faster per call for single-resource lookups, GraphQL is dramatically faster for relational and grid loads, and the cacheability story is the deciding factor at scale. The decision is not REST or GraphQL — it is which pattern fits which call site.[1]

REST wins single-resource lookups, GraphQL wins everything relational

That is the headline from 600 timed curl runs against a production-grade Magento 2.4.9 store. The 50,000-SKU catalog runs on PHP 8.4, MariaDB 11.4, OpenSearch 2.12, Redis 7.2, Varnish 7.5 behind nginx 1.27, Hyvä 1.3 theme. Calls are unauthenticated where possible, customer-token authenticated otherwise. Sub-1 ms RTT — numbers below are server-side cost, not wide-area latency.

REST is a URL. GraphQL is a query plan. Cache only understands URLs, which is why GraphQL caching needs deliberate help and REST caching usually does not.

Each query below is a real call site that headless and hybrid Magento storefronts make every minute. The exact curl, query body, and wrk concurrency test sit below each section.

Query A: get a single product by SKU

The cheapest possible API call — a PDP component on a JavaScript storefront needs name, price, image, attributes, stock state.

The REST call

curl -s -o /dev/null -w '%{time_total}\n' \
  -H 'Authorization: Bearer ' \
  'https://example.com/rest/V1/products/SKU123'
# median of 200 runs: 95 ms — p95: 138 ms — p99: 211 ms

REST resolves a single SKU through Magento\Catalog\Api\ProductRepositoryInterface::get() — a primary-key lookup on catalog_product_entity followed by EAV joins. Bootstrap is ~30 ms on warm OPcache and the repository call ~55 ms.

The GraphQL call

query GetProductBySku($sku: String!) {
  products(filter: { sku: { eq: $sku } }) {
    items {
      sku name url_key stock_status
      price_range { minimum_price { final_price { value currency } } }
      small_image { url label }
    }
  }
}
curl -s -o /dev/null -w '%{time_total}\n' \
  -X POST 'https://example.com/graphql' \
  -H 'Content-Type: application/json' \
  -d '{"query":"query($sku:String!){products(filter:{sku:{eq:$sku}}){items{sku name}}}","variables":{"sku":"SKU123"}}'
# median of 200 runs: 140 ms — p95: 198 ms — p99: 282 ms

GraphQL is 45 ms slower despite returning the same data — roughly 20 ms schema validation, 15 ms resolver dispatch, 10 ms response shaping. products() is also a search call, not a primary-key lookup, so it hits the EAV layered-navigation stack with a single-SKU filter.[2]

Winner: REST by 32%. On a PDP that loads a single product, REST is the correct call.

Query B: customer orders with line items

The call site where GraphQL stops being a curiosity and becomes mandatory. A My Account dashboard needs the customer's last 50 orders with line items.

The REST shape — N+1

# Step 1: get the order list (one call)
curl -s -H 'Authorization: Bearer ' \
  'https://example.com/rest/V1/orders?searchCriteria[pageSize]=50' > orders.json
# 220 ms — returns 50 order headers, no items

# Step 2: loop 50 orders to fetch items (50 calls)
for id in $(jq -r '.items[].entity_id' orders.json); do
  curl -s -H 'Authorization: Bearer ' \
    "https://example.com/rest/V1/orders/$id" > /dev/null
done
# 50 × ~60 ms = 3,000 ms sequential

The default /rest/V1/orders endpoint returns order headers, not items. Sequential cost: 3,220 ms.

The GraphQL shape — one query

query CustomerOrders {
  customer {
    orders(pageSize: 50) {
      items {
        number order_date status
        total { grand_total { value currency } }
        items { product_sku product_name quantity_ordered product_url_key }
      }
    }
  }
}
curl -s -X POST 'https://example.com/graphql' \
  -H 'Authorization: Bearer ' \
  -H 'Content-Type: application/json' \
  -d @customer-orders.graphql
# median of 200 runs: 220 ms — p95: 310 ms — p99: 415 ms

GraphQL resolves the sales_ordersales_order_item relationship at the resolver layer — OrderItemTypeResolver does a single batched join keyed by parent order ID, the dataloader pattern GraphQL was designed for.[3] One round trip, 220 ms.

Winner: GraphQL by 14.6×. There is no REST path that catches up unless you ship a custom join endpoint, which means writing module code and accepting an upgrade burden.

Query C: homepage 12-product grid

The storefront's homepage shows a grid of 12 featured products — same shape as Query A, but repeated.

The REST shape

# Single search call with searchCriteria
curl -s 'https://example.com/rest/V1/products?searchCriteria[filterGroups][0][filters][0][field]=category_id&searchCriteria[filterGroups][0][filters][0][value]=15&searchCriteria[pageSize]=12'
# median: 280 ms — returns 12 products with attributes

# Or, more commonly, the storefront fetches by SKU list:
for sku in SKU001 SKU002 SKU003 SKU004 SKU005 SKU006 \
           SKU007 SKU008 SKU009 SKU010 SKU011 SKU012; do
  curl -s "https://example.com/rest/V1/products/$sku" > /dev/null
done
# 12 × 55 ms = 665 ms sequential

The search-criteria call is reasonable at 280 ms, but the loop-by-SKU pattern — what most JavaScript storefronts default to when the homepage curator picks SKUs by hand — is 665 ms.

The GraphQL shape

query Homepage($skus: [String!]!) {
  products(filter: { sku: { in: $skus } }) {
    items {
      sku name url_key stock_status
      small_image { url label }
      price_range { minimum_price { final_price { value currency } } }
    }
  }
}

One query, 12 items, 180 ms. The sku: { in: [...] } filter compiles to a single WHERE sku IN (...) clause on catalog_product_entity with one EAV join pass. Request body ~600 bytes, response ~6 KB.

Winner: GraphQL by 3.7×. Even compared to the optimised REST search-criteria call (280 ms), GraphQL is 35% faster because it returns only the fields the storefront asked for.

The full comparison table

The numbers below are median over 200 sequential runs, single client, sub-1 ms network latency, Magento 2.4.9 + PHP 8.4 + OPcache + Redis warm, Varnish cold (cold cache is the worst case — see the cache section below for the warm-Varnish numbers).

QueryREST costGraphQL costWinner
A: single product by SKU95 ms140 msREST (1.5×)
B: 50 orders with items3,220 ms (N+1)220 msGraphQL (14.6×)
C: 12-product homepage grid (loop)665 ms180 msGraphQL (3.7×)
C': same grid via REST searchCriteria280 ms180 msGraphQL (1.6×)
D: PDP with related + upsell + crosssell4 calls, ~360 ms1 call, 260 msGraphQL (1.4×)
F: stock check by SKU (high frequency)40 ms (stockItems)110 msREST (2.7×)

Single-resource calls (A, F) favour REST by a clean margin. Relational or grid-shaped (B, C, D) favours GraphQL. Call-site shape is the deciding factor, not personal preference.

The cache angle — where production behaviour diverges

Benchmark numbers above are origin-server cost. In production, both responses go through Varnish — and the cacheability story is where REST quietly wins back ground.

REST is FPC-cacheable by URL

Magento's Varnish VCL caches REST GET responses for endpoints that opt in via X-Magento-Tags. The cache key is URL plus Store. A /rest/V1/products/SKU123 response stays cached until product save invalidates cat_p_123.

curl -I 'https://example.com/rest/V1/products/SKU123'
# x-magento-tags: cat_p_123,cat_p
# x-magento-cache-debug: HIT
# age: 47
# cache-control: max-age=86400, public, s-maxage=86400

Warm-Varnish median for Query A drops from 95 ms to 9 ms — served from Varnish without touching PHP-FPM.

GraphQL needs the @cache directive plus VCL help

GraphQL POSTs are not URL-cacheable — every request has a different body. Magento ships two mechanisms: the @cache directive on cacheable queries, plus a Varnish VCL extension that hashes the query body into the cache key.[4]

# app/code/Magento/CatalogGraphQl/etc/schema.graphqls
type Query {
  products(search: String, filter: ProductAttributeFilterInput, pageSize: Int = 20): Products
    @resolver(class: "...")
    @cache(cacheable: true, cacheIdentityClass: "...CategoryProductsIdentity")
}

The VCL — built into core since 2.4.4 — requires GET for cacheable queries, hashes the query string into the key, and emits X-Magento-Tags.

curl -G 'https://example.com/graphql' \
  --data-urlencode 'query={products(filter:{sku:{eq:"SKU123"}}){items{sku name}}}' \
  -H 'Store: default' -I
# x-magento-cache-debug: HIT
# age: 122

Warm-Varnish median for Query A on GraphQL drops from 140 ms to 11 ms — only if the storefront uses GET for cacheable queries, which most Apollo and urql clients do not without explicit configuration.

Customer queries are uncacheable in both

Query B is uncacheable in both worlds — session-dependent, Cache-Control: private. The 220 ms GraphQL number is the actual cost every request. The 3,220 ms REST N+1 path consumes 51 PHP-FPM workers instead of one — on a pool of 35, a single My Account page load saturates it.

Authentication overhead

Both APIs accept Authorization: Bearer <token>. The bearer-token validation path in Magento\Webapi\Controller\Rest\RequestValidator does a database lookup on oauth_token per request — ~12 ms overhead on every authenticated call. For high-frequency storefront calls (homepage grid, PDP), unauthenticated reads via the integration token configured server-side are the right call. For customer-specific data (cart, orders, account), the overhead is unavoidable.

Mobile versus storefront trade-offs

Mobile networks add 50 — 200 ms RTT per call. A 4-call REST sequence on 4G is 4 × (95 + 120 ms RTT) = 860 ms; one GraphQL call is 300 ms. Mobile flips Query A — REST is faster server-side, but round-trip cost erases the 45 ms saving the moment one related call is added. Mobile-first headless storefronts default to GraphQL; server-rendered Hyvä storefronts where calls happen server-to-server stay competitive on REST for single-resource calls.

wrk concurrency — what happens under load

Median latency on a single client is half the story. Under load, the picture shifts.

wrk -t4 -c50 -d30s 'https://example.com/rest/V1/products/SKU123'
# Requests/sec: 412.18, p50: 121 ms, p99: 387 ms, pool peak 28/35

wrk -t4 -c50 -d30s -s graphql-post.lua 'https://example.com/graphql'
# Requests/sec: 287.40, p50: 174 ms, p99: 521 ms, pool peak 34/35 — saturated

REST sustains 30% more requests per second and saturates the PHP-FPM pool less aggressively under 50 concurrent connections. The GraphQL resolver path has more per-request memory cost; with opcache.preload configured the gap narrows to ~10%.

The decision matrix

Call siteUse RESTUse GraphQL
PDP single productYesOnly if part of multi-fetch
Category product gridAcceptablePreferred (field selection)
Customer orders with itemsNeverYes
Cart with line items + totalsPossible (2 calls)Yes (1 call)
Checkout payment methods + shippingAcceptablePreferred
Server-to-server integration (ERP sync)Yes (URL-cacheable)Avoid
Stock check by SKU (high frequency)YesAvoid
Multi-store federationPer-store endpointsSingle endpoint with Store header

Most Magento 2.4.4 — 2.4.9 storefronts I have shipped through kishansavaliya.com end up using both — REST for high-frequency single-resource calls that benefit from URL-keyed FPC, GraphQL for relational and field-selective calls that would otherwise be N+1.

FAQ

Should I use REST or GraphQL for a new Hyvä storefront?

Hyvä is server-rendered, so most data fetching happens in PHP on the same box — REST is fine for the bulk of it because the cross-network round trip is sub-millisecond. The GraphQL endpoint becomes the right call for Alpine.js components that fetch on the client (mini-cart, customer-data, account dashboard) — those benefit from GraphQL's field selection and single round trip.

Is GraphQL always slower per call than REST?

No. For single-resource lookups by primary key, REST is ~30% faster because the resolver path is shorter. For anything relational, GraphQL is faster by a wide margin because REST forces N+1 calls. The cross-over point is around 2 — 3 related resources per logical view.

Does Varnish cache GraphQL responses?

Yes, but only for queries marked @cache(cacheable: true) and only when the client uses GET instead of POST. The cache key includes the query body hash plus the Store header. Customer-specific queries (cart, orders) are correctly marked uncacheable and Varnish passes them through.

How do I check whether my GraphQL query is hitting Varnish?

Send the query as GET with --data-urlencode and check the response headers. x-magento-cache-debug: HIT and a non-zero age header confirm a Varnish hit. If you only see MISS, check that the query has @cache in schema.graphqls and that the client is not adding cache-busting query params.

What is the N+1 query problem in REST?

It is the pattern where listing a collection of N resources requires 1 list call plus N detail calls to fetch nested data. Magento REST exhibits this for orders with items, customers with addresses, and any parent-child relationship. GraphQL avoids it because the resolver layer batches the nested lookup into a single SQL query keyed by parent ID.

Does authentication overhead affect REST and GraphQL equally?

Yes — the ~12 ms bearer-token validation cost is identical because both APIs go through the same Magento\Webapi\Controller validator. Customer-token and admin-token validation both hit oauth_token on every authenticated request. Server-to-server integration tokens are cheaper because they can be validated against an in-memory list.

Which is better for mobile apps — REST or GraphQL?

GraphQL — by a wide margin. Mobile networks add 50 — 200 ms of round-trip latency per call, which means a 4-call REST sequence on 4G can be slower than a 1-call GraphQL query even when the GraphQL call is technically more expensive server-side. The cross-over point on mobile is one related resource.

References

  1. Production engagement benchmarks from Magento 2 API consulting work, January — May 2026. Runs collected with curl %{time_total} and wrk 4.2.0, single client, sub-1 ms RTT to origin.
  2. Adobe Commerce DevDocs, REST API reference — Catalog, /rest/V1/products/{sku} service contract on ProductRepositoryInterface.
  3. Adobe Commerce DevDocs, GraphQL Developer Guide — Customer orders query, including the dataloader pattern used by OrderItemTypeResolver.
  4. graphql.org, Caching GraphQL queries — guidance on GET for cacheable queries and HTTP cache key construction; complemented by Adobe Commerce GraphQL caching with Varnish documentation.
  5. PHP 8.4 OPcache documentation — opcache.preload behaviour as it affects GraphQL resolver instantiation cost.
Picking the right API surface for your storefront?

I am Kishan Savaliya, an Adobe-Certified Magento + Hyvä developer. I run fixed-scope API performance engagements: baseline curl + wrk capture, REST/GraphQL call-site audit, Varnish VCL tuning for GraphQL @cache directives, and a 30-day post-deploy monitoring window. Fixed quote from $499 audit · $2,499 sprint · ~28h @ $25/hr. See Magento 2 performance optimization or hire me.