Learn GraphQL - Client & Server-Side Caching Strategies
Episode 20 of 51

Learn GraphQL - Client & Server-Side Caching Strategies

Episode 20 builds an end-to-end caching architecture: server-side caching with cache warming and invalidation, cache layers from CDN to Redis, Apollo Client's normalized cache with type policies, cache update and eviction patterns, and an introduction to GraphQL CDNs.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Caching is one of the most effective tools for speeding up an application — and also one of the sneakiest sources of bugs. Episode 20 builds a proper end-to-end caching architecture: from the server side to the client side, with an understanding of normalization, invalidation, and eviction. We'll cover server-side caching, the available cache layers, Apollo Client's normalized cache, how to update and evict the cache, and an introduction to GraphQL CDNs.

Server-Side Caching

Full Response and Partial Caching

  • Full response caching: the entire query response is stored and returned for the same query. Effective for rarely-changing public data.
  • Partial caching: some fields are cached (field-level, episode 19) and the rest is fetched directly.
JSFull response caching in Redis
async function cachedQuery(operation, key) {
  const cached = await ctx.redis.get(key);
  if (cached) return JSON.parse(cached);
 
  const result = await execute(operation);
  await ctx.redis.set(key, JSON.stringify(result), "EX", 60);
  return result;
}

Cache Warming and Invalidation

  • TTL (time-to-live): data expires automatically after a set period.
  • Event-based: the cache is deleted/updated when a mutation changes related data.
JSInvalidation on a mutation
async function createPost(_, args, ctx) {
  const post = await ctx.db.posts.create(args.input);
  await ctx.redis.del("feed:home");
  await ctx.redis.del(`posts:${args.input.authorId}`);
  return post;
}

Cache Layers

CDN, Application, and Database

  • CDN (Cloudflare, Fastly): stores responses at the edge, best for public data.
  • Application-level cache: stores query or field results in application memory (Redis, Memcached).
  • Database query cache: caches database query results for repeated access patterns.
Run Redis via Docker
docker run -d -p 6379:6379 redis:7

Common practice: combine layers with rules — cache query results for public data on the CDN, authentication and per-user data in Redis (run via docker run -d -p 6379:6379 redis:7), and avoid application-level caches that are hard to invalidate centrally.

Client-Side Caching

Apollo Client InMemoryCache

The client side has its own cache — this is what makes the UI feel instant. Apollo Client stores query results in a normalized cache, storing each object once based on __typename and id:

JSInMemoryCache setup
import { ApolloClient, InMemoryCache } from "@apollo/client";
 
const client = new ApolloClient({
  uri: "http://localhost:4000",
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            keyArgs: ["category"],
          },
        },
      },
    },
  }),
});

The normalized cache stores each object once based on __typename and id, then links them — so two queries sharing the same object automatically share data without refetching.

Cache Policies

  • cache-first (default): use the cache when available; network only if empty.
  • cache-only: only the cache, no network.
  • network-only: always the network, ignore the cache.
  • cache-and-network (default for feed UIs): show the cache immediately, then update from the network.

Cache Normalization

Type Policies and Custom Cache IDs

Normalization works by combining __typename and id. For types without an id, configure keyFields so they can still be normalized. Without a stable key, the same query will be fetched repeatedly — a common cause of "the cache isn't working" in Apollo Client:

JSCustom cache ID
const cache = new InMemoryCache({
  typePolicies: {
    Review: {
      keyFields: ["id"],
    },
    Person: {
      keyFields: ["firstName", "lastName"],
    },
  },
});

Without a stable key, objects aren't normalized and the same query will be fetched repeatedly. This is a common cause of "the cache isn't working" in Apollo Client.

Cache Updates and Eviction

Updating the Cache After a Mutation

  • Refetch: re-run the affected query — simple but wasteful.
  • Cache write: update the cache directly from the mutation result.
JSUpdate the cache after a mutation
const [createPost] = useMutation(CREATE_POST, {
  update(cache, { data }) {
    const existing = cache.readQuery({ query: GET_POSTS });
    cache.writeQuery({
      query: GET_POSTS,
      data: {
        posts: [data.createPost, ...existing.posts],
      },
    });
  },
});

Eviction and Optimistic UI

To drop data from the cache (for example, on logout), call cache.evict and cache.gc:

JSEvict and garbage collect
client.cache.evict({ id: `User:${userId}` });
client.cache.gc();

Optimistic UI displays temporary changes before the server responds (episode 5): Apollo stores the optimistic result in the cache, then replaces it with the real result when the mutation completes. This gives an instant UI experience.

GraphQL CDN

Stella and GraphCDN

A GraphQL CDN stores query results at the edge network, drastically lowering latency for global users. Services like Stella (Netlify) and GraphCDN connect directly to a GraphQL origin:

Set up GraphCDN via CLI
npx graphcdn push

The configuration is usually a YAML file marking which queries may be cached and the purging rules:

graphcdn.yml
originUrl: https://api.kalian.com
scopes:
  - operationName: HomeFeed
    maxAge: 60

The CDN cache is purged when data changes — usually via a webhook from mutations. This completes the caching architecture: CDN in front, Redis in the middle, and a normalized cache on the client.

Conclusion

Key takeaways:

  • Server-side caching consists of full response and partial caching with event-based invalidation.
  • Cache layers are tiered: CDN for public data, Redis for per-user data.
  • Apollo Client uses a normalized cache with type policies and keyFields.
  • Cache policies control the balance between speed and data freshness.
  • Manual cache updates and optimistic UI keep the UI consistent.
  • GraphQL CDNs bring the cache to the edge for low global latency.

In the next episode, episode 21, you'll learn about testing GraphQL — the test pyramid, unit testing resolvers with mocks, integration testing with Apollo Server testing and supertest, schema testing with GraphQL Inspector, data mocking, and E2E testing with Cypress. Your API quality will be verified from unit all the way to end-to-end!

Learn GraphQL - Client & Server-Side Caching Strategies | Learn GraphQL