If you're using Hashnode's GraphQL API to fetch your blog posts for a custom frontend (like a React or Next.js portfolio), you've probably run into this incredibly frustrating issue: You publish a new post on Hashnode, but it doesn't show up on your website.
You check the API payload, and it's serving a stale list of posts. The new post is completely missing.
I spent hours debugging this, trying every cache-busting trick in the book. Here's what didn't work, why it failed, and the actual simple solution.
The Setup
My portfolio runs on React (Vite) and uses Hashnode as a headless CMS. I fetch posts using fetch with the official Hashnode GraphQL endpoint: https://gql.hashnode.com.
const res = await fetch('https://gql.hashnode.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: POSTS_QUERY, variables }),
});
When I added a new post, my local dev server still only showed the old posts.
The Failed Attempts
Hashnode uses Stellate, a powerful GraphQL Edge CDN. Stellate sits between your frontend and Hashnode's database, caching responses to make the API blazing fast. However, its caching mechanism is exceptionally aggressive.
Here are the standard tricks I tried to bypass the cache, all of which failed:
- Setting standard HTTP headers: Adding
Cache-Control: no-cacheandPragma: no-cacheto thefetchcall. Stellate ignores these from the client. - URL Query Params: Appending a timestamp
?_t=${Date.now()}to the endpoint URL. Since this is aPOSTrequest, Stellate keys the cache off the request body, not just the URL. - GraphQL
extensions: Addingextensions: { cacheKey: Date.now() }to the JSON body. Stellate normalization strips this out. - Unknown GraphQL Variables: Injecting
_cacheBust: Date.now()into thevariablesobject. Stellate strips unknown variables. - GraphQL Comments: Injecting
# ${Date.now()}directly into the query string. Stellate parses and normalizes the query string, stripping comments before hashing the cache key.
No matter what I did, inspecting the network tab always revealed the same mocking response header: gcdn-cache: HIT.
The Real Issue: Stellate Needs the id Field
The root cause isn't that Stellate is ignoring your cache-busting hacks; it's that Stellate doesn't know the data is stale.
Stellate automatically invalidates its cache when backend data changes (mutations). But to do this effectively across complex GraphQL graphs, it relies on a core concept: tracking Node IDs.
If your query doesn't ask for the id of the entities it's fetching, Stellate cannot trace the cached data back to the actual database records. When Hashnode updates its database, Stellate's purge mechanism fires, but if your cached query didn't include IDs, Stellate doesn't know to invalidate that specific query.
This is a well-known issue internally at Hashnode — they even created an ESLint plugin (require-id-when-available) for their own engineers to prevent it!
The Solution
The fix is almost disappointingly simple. You must include the id field in all your GraphQL queries and fragments related to Hashnode.
My original query looked like this:
query Publication($host: String!, $first: Int!) {
publication(host: $host) {
posts(first: $first) {
edges {
node {
slug
title
brief
publishedAt
}
}
}
}
}
The fix is simply adding id inside the node:
query Publication($host: String!, $first: Int!) {
publication(host: $host) {
id # <-- ESSENTIAL FOR LIST INVALIDATION
posts(first: $first) {
edges {
node {
id # <-- ESSENTIAL FOR ENTITY INVALIDATION
slug
title
brief
publishedAt
}
}
}
}
}
Make sure to add id to everywhere you query entities: the parent publication object, posts, post, seriesList, etc.
Why both?
- Adding
idto thenodetells Stellate to update the cache for that specific post if it gets edited. - Adding
idto the parentpublicationtells Stellate to invalidate the entire list of posts for that publication when a new post is published or deleted.
Once I updated my queries and refreshed, the CDN properly registered the unique records and lists. New posts now invalidate the cache automatically, and the API returns fresh data immediately upon publishing.
Takeaway: When working with GraphQL APIs behind Edge CDNs like Stellate, always fetch the id. It's not just good practice; it's the anchor for their entire caching strategy.
Top comments (2)
Oh man, I FEEL THIS PAIN. 😭 Cache issues are the absolute worst because they make you question EVERYTHING — your code, your sanity, the laws of physics.
'You publish a new post... but it doesn't show up.' That moment when you refresh 47 times and stare at the screen like it personally betrayed you. Been there!
Thank you for documenting the solution! GraphQL caching is such a nightmare because it's not always obvious WHERE the cache is happening — client, CDN, server? Stellate sounds like a lifesaver.
Quick question: Did you try any other CDNs before settling on Stellate? Or was this the first solution that actually worked? 🤔
"Haha, the 47 refreshes is exactly what happened to me! It feels like you're going insane. 🤣
To answer your question: I actually didn't choose Stellate! Stellate is the Edge CDN that Hashnode uses under the hood by default for their GraphQL API. So if you are using Hashnode's headless CMS, you are automatically passing through Stellate whether you know it or not!
That's what makes this bug so frustrating—you have zero control over the CDN configuration, so you have to play by their rules (like fetching the id field) to trigger their internal cache purges.
Hope that clarifies things, and definitely keep an eye out for missing id fields in your future GraphQL projects! Cheers! 🍻"