Cache Invalidation Methods

Caching is the simple part. Cache invalidation, however, is one of the hardest problems in computer science.

While you can cache all over your stack, from the browser to the database, with Stellate’s edge cache, you cache right between your user and your backend to provide data to the user at maximum speed. With Stellates edge cache, any already cached request is fast for your user, as we’re caching it geographically close to them.

But what if the data changed? How do we make sure that the cache is updated?

Stellate’s Cache Invalidation Methods

There are a couple of options to invalidate the cache:

  1. TTL-based: You set a max-age and/or swr (stale-while-revalidate) value, which Stellate applies to specific types (or fields) that then cause matching responses to get cached. Once the time you configure is passed, the next request for that same query will cause Stellate to re-fetch the query from your backend and update the cache.
    While this is easy to configure, you might see stale data quite often. Let’s say you set a max-age: 3600 which would cache responses for one hour, but someone just changed a post on your platform. You want to make sure, that this post is updated immediately, not an hour later.
    Where is this useful? Let’s say you have a scraping cronjob that runs every 30min. In that case, you control the data updates and know that it only updates every 30 minutes - setting a max-age: 1800 is a reasonable approach here.
  2. Mutation-based: Similar to how the cache gets updated in a GraphQL client, Stellate invalidates all cached documents based on the response to any mutation that passes through our systems.
  3. Manual: Sometimes, you need to manually invalidate specific cached responses. You might for example have a webhook that changes data outside of your GraphQL request flow. In that case, you need to explicitly tell Stellate, that the data has changed.

When to use what

Now that you know which options are available, when should you use which cache invalidation option?

TTL-based (Time to live)

max-age and stale-while-revalidate are the easiest invalidation techniques Stellate supports. But when to use them? When can you solely rely on them without additional invalidation? And when not?

✅ When to use

  • Data only changes in specific time intervals. For example, a cronjob that updates data every 30min → max-age: 1800
  • If you’re fine with stale data. Let’s say you have an online shop on Black Friday and it’s more important that it’s available and fast. A good approach here is to use a combination of max-age and swr (stale-while-revalidate).

❌ When not to use

  • If you are not fine with stale data and it changes more frequently, you need additional invalidation techniques.

Read more about how to use TTL-based invalidation in our documentation on Cache Rules.

Mutation-based

As we discussed earlier, TTL-based cache invalidation is not always enough. Stellate offers automated mutation-based invalidation with 3 different policies:

  • Entity: Invalidate any cached response that contains the specific entity with the specific ID returned by the mutation, for example, the User with id: 3
  • List: Extends the Entity policy and additionally invalidates all cache responses that contain a list of the specific type returned by the mutation.
  • Type: This is the most aggressive policy. It invalidates all documents containing a certain type, no matter if there is an overlap in entities between the mutation response and the document, or not.

You can change the policy in your configuration file:

import { Config } from 'stellate'

const config: Config = {
  config: {
    name: 'my-app',
    schema: 'https://api.my.app',
    originUrl: 'https://api.my.app',
    mutationPolicy: 'Entity'
  },
}
export default config

Examples

Let's look at those policies in more detail via a couple of examples. Assuming the following mutation and GraphQL response.

mutation {
  updateLaunch(id: 108, mission_name: "Stellate Edge Cache Expansion") {
    __typename
    id
    name
  }
}
{
  "data": {
    "updateLaunch": {
      "__typename": "Launch",
      "id": "108",
      "mission_name": "Stellate Edge Cache Expansion"
    }
  }
}

Entity based invalidation

Given the below query and GraphQL response, since the Launch with ID 108 is included in the response it would get invalidated. When you next send that same query, it would trigger a request to your backend and add the response to the cache again.

query {
  launchesPast(limit: 2) {
    __typename
    id
    mission_name
    launch_date_utc
  }
}
{
  "data": {
    "launchesPast": [
      {
        "__typename": "Launch",
        "id": "109",
        "mission_name": "Starlink-15 (v1.0)",
        "launch_date_utc": "2020-10-24T15:31:00.000Z"
      },
      {
        "__typename": "Launch",
        "id": "108",
        "mission_name": "Sentinel-6 Michael Freilich",
        "launch_date_utc": "2020-11-21T17:17:00.000Z"
      }
    ]
  }
}

List based invalidation

The following query doesn't return the Launch with ID 108, however it returns a list of Launches. WIth the Listbased invalidation, the response would be invalidated and purged from the cache.

Although the following GraphQL response does not contain the launch with the id 108, it will be purged, as it contains a list of type Launch.

query {
  launchesPast(limit: 2, offset: 2) {
    __typename
    id
    mission_name
    launch_date_utc
  }
}
{
  "data": {
    "launchesPast": [
      {
        "__typename": "Launch",
        "id": "107",
        "mission_name": "Crew-1",
        "launch_date_utc": "2020-11-16T00:27:00.000Z"
      },
      {
        "__typename": "Launch",
        "id": "106",
        "mission_name": "GPS III SV04 (Sacagawea)",
        "launch_date_utc": "2020-11-05T23:24:00.000Z"
      }
    ]
  }
}

Type based invalidation

As mentioned earlier, Type based invalidation is the most aggressive policy. The following response doesn't include the Launch with ID 108, and it also doesn't include a list of Launches either. However, it does include a Launch (in this case with ID 107) and thus will get invalidated as well.

{
  launch(id: "107") {
    __typename
    id
    mission_name
    launch_date_utc
  }
}
{
  "data": {
    "launch": {
      "__typename": "Launch",
      "id": "107",
      "mission_name": "Crew-1",
      "launch_date_utc": "2020-11-16T00:27:00.000Z"
    }
  }
}

✅ When to use

  • Entity: A good use-case for entity-based mutation invalidation is updating a specific entity, for example with an updateUser mutation, where you know, it won’t have side effects of re-ordering other queries.
  • List: When any of the CRUD mutations, Create, Update or Delete change data that might have side effects on any list query with the particular type, the List policy is a good match. Note that this is eager invalidation, and it will invalidate unrelated lists.
  • Type: Sometimes you don’t know which queries are affected by a mutation. Then you can use the Type policy, which will invalidate any query that contains a certain type.

❌ When not to use

  • Sometimes, mutations have side effects, that are not represented in the mutation payload. You might for example just have a boolean as a return type.
  • Any time the mutation response is not directly connected to the types it changes

You can learn more about this in our documentation on Automatic Purging based on Mutations.

Manual Invalidation

The most flexible approach is to integrate with Stellates Purging API directly. Via the API you have full control over which data is invalidated and when it is invalidated. And we offer a wide range of invalidation methods, that range from purging individual entities (e.g. purgeLaunch(id: 108)), to the complete cache (e.g. _purgeAll()).

✅ When to use

  • You should first ask yourself if TTL-based or mutation-based invalidation can do the job for you. If that's not the case and you have the time to implement the Purging API, go ahead.
  • Any modifications to your data that happen outside of your GraphQL workflow and need to be reflected immediately, e.g. webhooks, or cron jobs that write directly to the database.

❌ When not to use

  • If the other invalidation methods are suitable for your service, we'd recommend using them, as they’re simpler and easier to implement.