GraphQL Caching

Caching stores the result of expensive operations so future requests can receive the same data instantly without repeating the work. GraphQL's flexibility makes HTTP-level caching harder than REST, but there are several targeted strategies that work well in practice.

Why HTTP Caching Is Harder with GraphQL

  REST caching:                         GraphQL caching challenge:
  ─────────────                         ──────────────────────────
  GET /products/42                      POST /graphql
  URL is unique per resource            All queries go to same URL
  CDN caches by URL automatically       POST bodies are not cached by CDNs
  Cache-Control header works            Must use other strategies

Strategy 1 – DataLoader (Request-Level Caching)

DataLoader caches database results within a single request. The same ID fetched twice in one query hits the database only once. This is not persistent caching — the cache resets with every request.

  Request lifetime:
  ──────────────────
  Start request  → DataLoader cache is empty
  Resolver loads user 5 → DB hit → cached in DataLoader
  Resolver loads user 5 again → served from DataLoader cache (no DB)
  Request ends   → DataLoader cache is discarded

Strategy 2 – Server-Side Cache with Redis

Store resolver results in Redis with a time-to-live (TTL). The next request for the same data reads from Redis instead of the database. This cache persists across requests and users.

  import { createClient } from 'redis';
  const redis = createClient();

  const resolvers = {
    Query: {
      product: async (_, args, context) => {
        const cacheKey = 'product:' + args.id;

        // 1. Check cache
        const cached = await redis.get(cacheKey);
        if (cached) return JSON.parse(cached);

        // 2. Cache miss — fetch from DB
        const product = await context.db.products.findById(args.id);

        // 3. Store in cache for 60 seconds
        await redis.setEx(cacheKey, 60, JSON.stringify(product));

        return product;
      }
    }
  };

Strategy 3 – @cacheControl Directive (Apollo Server)

Apollo Server supports a @cacheControl directive that annotates fields with cache hints. Apollo Gateway and CDN plugins use these hints to cache full query responses.

  Schema:
  ────────
  type Product @cacheControl(maxAge: 300) {   ← Cache for 5 minutes
    id:    ID!
    name:  String!
    price: Float! @cacheControl(maxAge: 30)  ← Price changes fast
  }

  type Query {
    product(id: ID!): Product @cacheControl(maxAge: 300)
  }

  The response carries a Cache-Control header:
  ─────────────────────────────────────────────
  Cache-Control: max-age=30
  (Uses the lowest maxAge from all fields in the query)

Strategy 4 – Persisted Queries

Persisted queries cache the query document on the server. Instead of sending the full query string in every request, the client sends only a hash. The server looks up the full query from its cache. This also enables GET requests, which CDNs can cache by URL.

  Normal request:            Persisted query request:
  ───────────────            ────────────────────────
  POST /graphql              GET /graphql?extensions=
  Body: 500-char query       {"persistedQuery":{"sha256Hash":"abc123"}}

  CDN: Cannot cache POST     CDN: Can cache GET by hash

What to Cache and for How Long

  Data type                    Cache TTL
  ─────────                    ─────────
  Static content (categories)  Long (hours to days)
  Product info                 Medium (minutes)
  Stock/price (changes often)  Short (seconds) or no cache
  User-specific data           Never cache at CDN level
  Auth tokens                  Never cache

Key Points

  • GraphQL cannot use HTTP GET caching by default because all queries use POST to a single endpoint.
  • DataLoader provides request-level caching automatically when used correctly.
  • Redis or Memcached provides persistent server-side caching for expensive queries.
  • @cacheControl directives annotate fields with cache duration hints for CDN-level caching.
  • Persisted queries enable CDN caching by converting POST queries into cacheable GET requests.

Leave a Comment