The case against normalized caching in GraphQL - WunderGraph
CEO & Co-Founder at WunderGraph
September 9, 2020·8min read
Last updated on July 18, 2026
Archive Notice
This article is archived and no longer maintained. It describes an earlier version of WunderGraph's client-side caching, which is no longer part of the current product. Some examples may not work as described. For current documentation and guidance, see https://wundergraph.com/cosmo
State of GraphQL Federation 2026
How are teams governing schema changes, handling production traffic, and measuring Federation success? Share your experience and get early access to the full report. For every valid survey completed, we'll donate $30 to UNICEF.
TL;DR
Rich GraphQL clients that ship a normalized cache make navigation instant and save bandwidth, but they are complex to build and maintain, run less efficiently in the browser's JavaScript VM than the browser cache, and force the frontend developer to keep cache state correct and implement custom eviction rules. They also introduce a second source of truth in the frontend that can show stale data, such as a friend who was already unfriended. The alternative is to push caching to the transport layer, where the Cosmo Router applies an HTTP Cache-Control policy so browsers and CDNs handle caching with a single source of truth and no cache-state code in the client. Most applications should avoid normalized-cache complexity and reach for an application-level cache like Redis or Memcached when performance is a concern.
Editor's note
This post was originally published in 2020 and updated in 2026 to reflect WunderGraph's current product. The normalized-caching analysis is unchanged; the caching examples now use the Cosmo Router's Cache-Control policy instead of the original WunderGraph client.
In this post we'll compare rich GraphQL clients that come with a normalized cache implementation against an approach that relies on HTTP caching at the transport layer.
With HTTP caching, the caching rules live at the transport layer. The Cosmo Router applies a Cache-Control policy to responses, set globally or per subgraph, so the response of an operation can be cached by the browser and any CDN in front of it. This mechanism is fully compatible with all major browsers and CDNs that implement caching according to the HTTP Caching RFC.
To illustrate this a bit better I'd like to introduce two example queries. The first one fetches a list of friends. The second one fetches some details about those friends.
Let's consider we want to show a list of friends:
1 2 3 4 5 6 7 8
For each friend we'd like to be able to click on the friend in the list and open up a detail page:
1 2 3 4 5 6 7 8
You will recognize that we already have all the data for the detail page. So in an ideal scenario the client won't have to make another request. This is possible thanks to cache normalization.
A smart normalized cache will identify the Friend entity and will recognize that the "FriendByID" query can be fulfilled using the data from the "Friends" query which we already ran.
Pros of Normalized Cache
- Navigating to a friend detail page will be instant because there is no network request required
- The client will save bandwidth, and the user experience will be more fluent
- If we navigate back we can also immediately pull out the list of friends from the normalized cache
Cons of Normalized Cache
A rich GraphQL client with a normalized cache is complex to build and maintain:
- The cache is running in the JavaScript VM of your browser and therefore less efficient than the browser cache
- The logic to keep the cache state correct can become quite hairy
- The frontend developer must understand the domain and program the cache correctly to avoid unwanted behaviour
- The frontend developer must implement custom rules for cache eviction
One thing that I want to explicitly mention outside of the list:
There's no single source of truth for the business object in this scenario.
The frontend developer might accidentally allow the UI to show a friend in the friends list even if you have previously unfriended said person. Errors like these are very hard to spot. I think we're giving the frontend developer a lot of responsibility in this case to get caching right.
Should it really be a concern of a frontend developer if data is stale? Shouldn't a frontend developer focus on the UI and trust the data layer? Does it really have to be that complicated to build rich apps with good performance?
I believe there are applications where it's definitely worth having such a complexity. On the other hand, I see many use cases, e.g., a news website, where relying on the HTTP Caching RFC is a lot simpler and more efficient.
Enter HTTP caching at the router:
With the Cosmo Router, caching lives in an HTTP Cache-Control policy that you set in configuration, globally or per subgraph, instead of hand-written cache logic in the client.
1 2 3 4 5 6 7
With this policy, a friend response can be cached for 5 seconds by the browser and any CDN in front of the router. When a request touches more than one subgraph, the strictest policy wins, and mutations are automatically marked no-cache so they are never stored. You define the behaviour once in config rather than teaching a client how to invalidate entities. If you want to take this further with a CDN like Cloudflare, Fastly, or Fly.io sitting in front of the router, see our guide to GraphQL CDN and edge caching.
Pros of HTTP Caching
- There's a single source of truth - the cache-control policy in your router config
- Caching is handled automatically by the browser which is easier to use and understand
- No complex tooling is required to understand why a request is cached; browsers have excellent debuggers for this
- No JavaScript code has to be written to keep the cache state in sync
- With a service worker, you can easily build offline apps using standard caching techniques
- Less JavaScript code to be run by the browser
- The frontend developer gets to focus on the UI and has to worry less about data fetching and caching logic
Cons of HTTP Caching for GraphQL Queries
- The client has to make more requests than with a normalized cache
- More requests lead to more bandwidth usage
- There's no easy way to invalidate the cache immediately
Conclusion
When you think you have to use a normalized cache in the client, you should consider an application level cache first.
A normalized cache introduces a second source of truth in the frontend which needs to be maintained. Most of the time you don't need this complexity and should avoid it. Keep it simple, solve a business problem, don't introduce technical debt.
The Cosmo Router gives you a simple and powerful way to apply transport-level caching through its Cache-Control policy. For 99% of the other use cases, you should consider adding an application-level cache if performance is an issue.