DEV Community

Cover image for How I Fixed the Hashnode GraphQL API Stale Cache Bug (Stellate CDN)
Vicente G. Reyes
Vicente G. Reyes

Posted on • Originally published at vicentereyes.org

How I Fixed the Hashnode GraphQL API Stale Cache Bug (Stellate CDN)

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 }),
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Setting standard HTTP headers: Adding Cache-Control: no-cache and Pragma: no-cache to the fetch call. Stellate ignores these from the client.
  2. URL Query Params: Appending a timestamp ?_t=${Date.now()} to the endpoint URL. Since this is a POST request, Stellate keys the cache off the request body, not just the URL.
  3. GraphQL extensions: Adding extensions: { cacheKey: Date.now() } to the JSON body. Stellate normalization strips this out.
  4. Unknown GraphQL Variables: Injecting _cacheBust: Date.now() into the variables object. Stellate strips unknown variables.
  5. 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
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Make sure to add id to everywhere you query entities: the parent publication object, posts, post, seriesList, etc.

Why both?

  • Adding id to the node tells Stellate to update the cache for that specific post if it gets edited.
  • Adding id to the parent publication tells 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)

Collapse
 
harsh2644 profile image
Harsh

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? 🤔

Collapse
 
highcenburg profile image
Vicente G. Reyes

"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! 🍻"