Episode 19 optimizes GraphQL performance: query analysis and bottleneck identification, database indexes, caching strategies and ETag, Automatic Persisted Queries, field-level caching with memoization, batching and DataLoader optimization, and query performance monitoring.

GraphQL gives clients tremendous flexibility — and that flexibility is often the culprit behind poor performance. Episode 19 covers GraphQL performance optimization systematically: finding bottlenecks, speeding up queries, and measuring the results.
We'll start with query analysis, then build the caching layers, Automatic Persisted Queries, field-level caching, batching optimization, and close with performance monitoring strategies.
Measure first before optimizing. Key metrics for every query:
The easiest tool: Apollo's tracing plugin to record each resolver's duration. The most common bottleneck patterns are N+1 (episode 9) and resolvers doing heavy work without caching.
Once you've found a slow query, check the database side:
where, orderBy, and foreign keys.CREATE INDEX idx_post_author_id ON posts(author_id);
CREATE INDEX idx_post_published_created ON posts(published, created_at DESC);Prisma lets you define indexes directly in schema.prisma with @@index, so migrations carry the indexes along.
Deterministic queries (no authentication, no side effects) can be cached at the CDN or HTTP layer. Apollo Server supports cache hints that mark parts of the response:
import { makeExecutableSchema } from "@graphql-tools/schema";
const schema = makeExecutableSchema({ typeDefs, resolvers });Then, in the resolver, set a hint via info.cacheControl or use the @cacheControl directive:
type Post {
id: ID!
title: String! @cacheControl(maxAge: 300)
}
type Query {
posts: [Post!]! @cacheControl(maxAge: 60)
}This produces a Cache-Control header on the response so CDNs and browsers can store query results. ETag adds more efficiency: the client sends If-None-Match and the server responds with 304 when data hasn't changed.
APQ splits a query into two steps: the first request sends the full query along with its hash, and the server stores the hash-to-query mapping. Subsequent requests send only the hash:
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: { ttl: 3600 },
});import { createHttpLink } from "@apollo/client/link/http";
const httpLink = createHttpLink({ uri: "/graphql", disableApq: false });The benefits: request payloads shrink dramatically (saving tens of percent of bandwidth), and queries don't need to be re-parsed on the server repeatedly. Details on configuring the APQ cache in Redis will be covered in episode 20.
For data that's read often and changes rarely, cache at the field level with a TTL:
const CACHE_TTL = 300;
async function getPost(id, ctx) {
const key = `post:${id}`;
const cached = await ctx.redis.get(key);
if (cached) return JSON.parse(cached);
const post = await ctx.db.posts.find(id);
await ctx.redis.set(key, JSON.stringify(post), "EX", CACHE_TTL);
return post;
}Cache invalidation is the hardest part. Common strategies: delete the key when the data is mutated (redis-cli DEL post:123), or use versioned keys (post:123:v2) bumped when the data schema changes. For highly dynamic data, adjust the TTL or skip the cache entirely.
DataLoader (episode 9) already handles per-request batching. Further optimizations:
const userLoader = new DataLoader(async (ids) => {
const cached = await ctx.redis.mget(ids.map((id) => `user:${id}`));
const missing = ids.filter((_, i) => cached[i] === null);
// fetch the missing ones, store them in the cache, then merge
});Optimization without measurement is just guessing. Implement:
Apollo Studio's "field usage" shows which fields clients actually use — the basis for cleaning up unused schema. Full observability integration, including OpenTelemetry and Prometheus, is covered in episode 24.
Key takeaways:
In the next episode, episode 20, you'll learn about caching architecture — full and partial server-side caching, cache layers from CDN to Redis, Apollo Client's normalized cache with type policies, cache updates and eviction, and GraphQL CDNs. Your end-to-end caching architecture will be complete!