GraphQL and Caching: More Than a Rumor
GraphQL has long faced a reputation for being incompatible with caching. The claim goes something like: “GraphQL doesn’t support caching,” and for teams evaluating query languages, that can be a dealbreaker. But the reality is more nuanced. GraphQL’s official documentation even discusses caching techniques, and a growing ecosystem of clients and services makes effective caching entirely achievable. Here’s how it works.
Automatic Caching With __typename
Consider a query fetching a post and its author:
query getPost {
post(slug: "working-with-graphql-caching") {
id
title
author {
id
name
avatar
}
}
}
The key to automatic caching lies in __typename, a meta field exposed by every GraphQL API. It returns the type name of each object in the response. Most GraphQL clients — such as urql — add __typename to outgoing queries automatically, whether on the client or at a CDN edge:
query getPost {
post(slug: "working-with-graphql-caching") {
__typename
id
title
author {
__typename
id
name
}
}
}
The resulting response now carries type information alongside the data:
{
data: {
__typename: "Post",
id: 5,
title: "Working with GraphQL Caching",
author: {
__typename: "User",
id: 1,
name: "Jamie Barton"
}
}
}
With __typename in place, you can cache this result and still know it contains a Post with ID 5 and a User with ID 1. This is the foundation for clients like Apollo and Relay, which include their own caching layers. Once the client knows what’s cached, it can serve subsequent queries from local cache instead of hitting the origin server.
Cache Invalidation Through Mutations
Now imagine the post author edits the title using an editPost mutation:
mutation {
editPost(input: { id: 5, title: "Working with GraphQL Caching" }) {
id
title
}
}
Because __typename is included automatically, the mutation response immediately signals that Post ID 5 has changed:
{
data: {
__typename: "Post",
id: 5,
title: "Working with GraphQL Caching"
}
}
Any cached query containing that post is now invalidated. The next identical query will fetch fresh data from the origin, avoiding stale responses.
Normalized Caching: How Clients Store Data
Many GraphQL clients don’t cache whole query responses. Instead, they normalize the data into two structures:
- An object store mapping each entity to its fields (e.g.,
Post #5: { … },User #1: { … }). - A query store mapping each query to the objects it references (e.g.,
getPost: { Post #5, User #1 }).
For detailed examples, urql’s normalized caching documentation and Apollo’s “Demystifying Cache Normalization” guide cover the specifics.
The Hard Case: List Updates
One edge case remains tricky for automatic GraphQL caching: adding items to a list. If a createPost mutation inserts a post, the cache can’t infer which list the new item belongs to. A practical workaround is to query a related parent type in the mutation. In the example below, the query fetches the community relation on post:
query getPost {
post(slug: "working-with-graphql-caching") {
id
title
author {
id
name
avatar
}
# Also query the community of the post
community {
id
name
}
}
}
The createPost mutation can also return that community, which invalidates any cached query results containing it:
mutation createPost {
createPost(input: { ... }) {
id
title
# Also query and thus invalidate the community of the post
community {
id
name
}
}
}
This approach isn’t perfect, but the combination of a typed schema and __typename gives GraphQL a solid foundation for caching.
Client-Side Limits and Edge Caching
It’s fair to admit that GraphQL doesn’t support traditional HTTP caching out of the box. Because GraphQL commonly operates over POST requests, typical browser caching doesn’t apply. Client-side “tricks” like the ones above are often required, and sometimes manual cache updates are unavoidable for complex scenarios.
Alternatively, a service like GraphCDN offers server-side edge caching. It also exposes a manual purging API for finer-grained control:
# Purge all occurrences of a specific object
mutation {
purgeUser(id: [5])
}
# Purge by query name
mutation {
_purgeQuery(queries: [listUsers, listPosts])
}
# Purge all occurrences of a type
mutation {
purgeUser
}
The benefit of edge caching is that you don’t need to reimplement cache logic across mobile, web, and other clients. A single cache is shared among users, reducing origin load and keepingresponses fast. Additional features like query complexity analytics may also be available at the service level.
Does GraphQL Care About Caching?
Yes. Whether through __typename-driven automatic invalidation, normalized client caches, or CDN-based edge caching, GraphQL has multiple viable caching strategies. The tooling may require more setup than REST’s URL-based caching, but the payoff is performance and reduced origin traffic — without losing GraphQL’s flexibility.



