DEV Community

Cover image for Caching Shopify GraphQL: A Practical Guide for Developers
Muhammad Masad Ashraf
Muhammad Masad Ashraf

Posted on • Originally published at kolachitech.com

Caching Shopify GraphQL: A Practical Guide for Developers

TL;DR: GraphQL can't be cached by URL like REST. Cache by query + variables, layer your caches (client → edge → app → persisted queries), match TTLs to data volatility, and invalidate via webhooks. Never cache carts or customer-specific pricing.


The Core Problem

REST caching is URL-based. One endpoint = one cache entry. Easy.

GraphQL uses a single endpoint for everything. The query body defines the response, so two requests to the same URL can return totally different data. URL-based caching is useless here.

Your cache key has to include the query + variables + user context.

// Naive (broken) approach
const key = endpoint; // same key for every query — wrong

// Correct approach
const key = hash(JSON.stringify({
  query: normalizedQuery,
  variables: sortedVariables,
  locale,
  buyerSegment
})); // correct
Enter fullscreen mode Exit fullscreen mode
Factor REST GraphQL
Endpoints Many One
Cache key URL Query + variables
Granularity Coarse Field-level possible
Invalidation Simpler More complex

The 4 Cache Layers

Don't think "a cache." Think layers, each catching a different request type.

  1. Client cache (Apollo / urql) — session reuse
  2. Edge / CDN cache — public storefront pages
  3. App cache (Redis / Memcached) — shared, semi-static data
  4. Persisted queries — stable hash-based keys

All of these sit in front of the Shopify GraphQL API.

Layer Location Best for Typical TTL
Client Browser/app Single session Session length
Edge/CDN Network edge Public data Minutes to hours
App Your server Shared data Seconds to hours
Persisted query Server Stable identity Long-lived

What to Cache (and What Will Burn You)

  • Cache hard: product details, collections, shop settings
  • Cache short: pricing, availability (a few seconds)
  • Never cache: carts, checkout, customer-specific pricing
Data Volatility Approach
Product details Low Cache hours, invalidate on update
Collections Low Cache hours
Inventory High Cache seconds or skip
Pricing Med-high Short TTL + invalidation
Cart/checkout Very high Don't cache
Customer data High + private Scope per user or skip

I once cached inventory too long and oversold during a launch. Learn from my pain.


Cache Invalidation: 3 Strategies

1. TTL (time-based) — simplest, but you're guessing the window.

await redis.set(key, payload, 'EX', 60); // expire in 60s
Enter fullscreen mode Exit fullscreen mode

2. Event-based (webhooks) — most accurate. Product updates fire a webhook, you purge the entry.

// products/update webhook handler
app.post('/webhooks/products/update', verifyHmac, async (req, res) => {
  const productId = req.body.id;
  await redis.del(`product:${productId}:*`);
  res.sendStatus(200);
});
Enter fullscreen mode Exit fullscreen mode

A dropped webhook means stale cache. Make your consumers reliable (retries, dead-letter queues).

3. Stale-while-revalidate — serve stale instantly, refresh in background.

Cache-Control: max-age=60, stale-while-revalidate=300
Enter fullscreen mode Exit fullscreen mode
Method Freshness Complexity Best for
TTL Medium Low Predictable data
Event-based High Med-high Inventory, pricing
SWR High Medium Public pages

Smart Cache Keys (don't leak data!)

For B2B stores with tiered pricing, the buyer's company must be in the key or you'll serve Customer A's contract price to Customer B.

function buildKey({ query, variables, context }) {
  return hash(JSON.stringify({
    q: normalize(query),
    v: sortObjectKeys(variables), // sort for consistency
    locale: context.locale,
    currency: context.currency,
    buyer: context.companyId ?? 'anonymous'
  }));
}
Enter fullscreen mode Exit fullscreen mode

Handling Personalized Data

Field-level splitting is the cleanest pattern:

  • Catalog data: cache once, globally
  • Cart + pricing: fetch fresh, per user, no cache

Keep your hit rate high where it counts, fetch fresh where it matters.


Measure It

  • Hit ratio = hits / (hits + misses) — maximize
  • Latency delta = before vs after — should drop
  • API calls avoided = cache hits — cost savings
  • Stale incidents = wrong price/stock reports — target zero

Common Mistakes

  • Caching personal data in a shared key
  • TTLs so long prices go stale
  • Ignoring webhook reliability
  • Caching mutation results

Wrap-Up

Layer your caches. Match TTLs to volatility. Invalidate via webhooks. Build keys that respect personalization. Measure, then tune.

Done right, caching turns a throttled, sluggish app into a fast, resilient one.

I wrote a longer, more detailed version with extra comparison tables and architecture notes here:
Caching Strategies for Shopify GraphQL

What's your go-to invalidation strategy? Drop it in the comments.

Top comments (0)