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? There are a couple of options to invalidate the cache:
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.
✅ 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
andswr
(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
, List
, and Type
. 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
Entity
based
Invalidate any cached response that contains the specific entity with the specific ID returned by the mutation, for example, the User
with id: 3
.
Example
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
Extends the Entity
policy and additionally invalidates all cache responses that contain a list of the specific type returned by the mutation.
Example
The following query doesn’t return the Launch
with ID 108, however it returns a list of Launch
es. WIth the List
based 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
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.
Example
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 Launch
es 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 anupdateUser
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, theList
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 theType
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
Manual
Sometimes, you need to manually invalidate specific cached responses. 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. 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.