How to Customize WPGraphQL Cache Keys 💾🔑

Francis Agulto Avatar

·

Caching is important in optimizing performance for headless WordPress setups. The WPGraphQL Smart Cache plugin helps manage caching for GraphQL queries, ensuring faster response times. In this guide, we’ll walk you through setting up your WordPress environment, installing the necessary plugins, and customizing GraphQL cache keys to better suit your specific needs.

Prerequisites

Before reading this article, you should have the following prerequisites checked off:

If you’re looking for a headless platform to develop on, you can get started with a WP Engine Headless WordPress sandbox site for free:

Understanding Default WPGraphQL Smart Cache Behavior

WPGraphQL Smart Cache automatically tags cached responses with keys derived from the GraphQL queries. These keys are linked to specific WordPress data (e.g., posts, pages, taxonomies). When relevant data is updated, the associated cache is invalidated. 

For example, a query that retrieves posts with specific categories and tags will generate cache keys like list:post, list:category, and list:tag. If any of these categories or tags are updated, the entire cache is invalidated, ensuring the data stays current.  

In addition to the list:$type_name keys, individual node IDs are also included. 

These individual IDs are used to purge cache when updates or deletes happen.

The list:$type_name is used to purge when a new thing is published.  For example list:post will be purged when a new post is published, but purge( "post:1" ) would be purged when post 1 is updated or deleted.   

Let’s see this in action.  Navigate to your WP admin, then open your WPGraphQL IDE. Copy and paste this query:

query GetPosts {
  posts {
    nodes {
      title
      uri
    }
  }
}

When you press play in your IDE, this will make a query to your site’s WPGraphQL endpoint. 

Then WPGraphQL will return headers that caching clients can use to tag the cached document. Next, Open your dev tools.  In this case, I am using Google Chrome.  When I open up the dev tools and inspect the response headers, you should see this:

Here, we see the X-GraphQL-Keys header with a list of keys. If this query were made via a GET request to a site hosted on a host that supports it, the response would be cached and “tagged” with these keys.

For this particular query, we see the following keys:

  • Hash of the Query: This is a unique identifier which is in this example

4382426a7bebd62479da59a06d90ceb12e02967d342afb7518632e26b95acc6f for the specific query made. It ensures that the exact same query returns the same cached response unless invalidated.

  • Operation Type (graphql:Query): Indicates that the operation is a GraphQL query, as opposed to a mutation or subscription
  • Operation Name (operation:GetPosts): Identifies the specifically named query, in this case, GetPosts, which helps in targeting this operation for caching or invalidation.
  • List Key (list:post): This key identifies that the query is fetching a list of posts. Any changes to the list of posts would trigger cache invalidation.
  • Node ID (cG9zdDox): This represents the specific node (e.g., a post) that was resolved in the query. Changes to this node will invalidate the cache for this query.

If a purge event for one of those tags is triggered, the document will be tagged with these keys and purged (deleted) from the cache.

Understanding Cache Invalidation with WPGraphQL Smart Cache

WPGraphQL Smart Cache optimizes caching by sending the keys in the headers, but the caching client (e.g., Varnish or Litespeed) needs to use those keys to tag the cache. WPGraphQL Smart Cache itself does not tag the cached document; it provides the caching client info (the keys) to tag the cached document with. A supported host like WP engine works with WPGraphQL Smart Cache out of the box.

Let’s discuss how invalidation works:

WPGraphQL Smart Cache listens to various events in WordPress, such as publishing, updating, or deleting content, and triggers cache invalidation (or “purge”) based on these events.

Detailed Key Breakdown:

Publish Events (purge('list:$type_name')): When a new post or content type is published, the cache for the entire list associated with that content type (e.g., all posts) is purged. This ensures that any queries fetching this list will be up-to-date.

  Update Events (purge('$nodeId')): When an existing post or content type is updated, the cache for that specific node (e.g., a single post) is purged. This allows the updated content to be fetched without affecting the entire list.

    Delete Events (purge('$nodeId')): Similarly, when a post or content type is deleted, the cache for that specific node is purged, ensuring that the deleted content is no longer served from the cache.

Why This Matters:

These targeted cache invalidations help maintain the balance between performance and data freshness. By only purging the cache when necessary and only for the relevant data, WPGraphQL Smart Cache ensures that users receive up-to-date content without unnecessary cache purges, which can negatively impact performance.

This invalidation strategy is crucial for optimizing the performance of headless WordPress setups using WPGraphQL, especially in dynamic environments where content changes frequently.

How Cache Invalidation and Cache Tags Work Together

Now that we’ve explored how cached documents are tagged and how cache invalidation works in WPGraphQL Smart Cache, let’s see how these concepts interact.

When a GraphQL query is executed, specific cache keys (tags) are associated with the cached response. These tags correspond to the data queried, such as posts, categories, or specific node IDs. The cache invalidation strategy then ensures that when relevant data changes occur in WordPress, the associated cached documents are purged based on these tags.

Example: Invalidation Scenarios for a GetPosts Query

  1. Publishing a New Post (purge('list:post')):
    • When a new post is published, the entire list of posts in the cache (tagged with list:post) is invalidated. This ensures that the new post will appear in any subsequent queries that fetch this list.
  2. Updating or Deleting a Specific Node (purge('$nodeId')):
    • If the “Hello World” post (with the ID cG9zdDox) is updated or deleted, the cache for that specific node is purged. This allows the updated or deleted content to be accurately reflected in any future queries.
  3. Manually Purging Cache (purge('graphql:Query')):
    • Clicking “Purge Cache” in GraphQL > Settings > Cache page triggers a manual cache purge for all queries. This can be useful when you want to ensure that all cached data is refreshed, regardless of specific events.
  4. Operation Name or Query Hash-Based Purge:
    • Custom purge events can be manually triggered based on the operation name (e.g., GetPosts) or the hash of the query. This level of control allows you to finely tune when and how caches are invalidated.

These strategies work together to ensure that the cache is only invalidated when necessary, providing up-to-date data without unnecessary performance overhead. For instance, when the “Hello World” post is updated, it’s reasonable to expect that the cache for the GetPosts query should be purged so that any queries return the most current data. This fine-grained control over cache invalidation ensures that your headless WordPress site remains performant while delivering fresh content.

Why Would You Need to Customize WPGraphQL Cache Keys?

In some scenarios, the default caching behavior might be too broad, leading to frequent cache invalidations.  This is especially true for more complex queries. 

For instance, if your query includes categories and tags, any update to these taxonomies will invalidate the cache, even if those changes don’t affect the specific posts you’re querying. Customizing cache keys allows you to fine-tune this behavior, ensuring that only relevant updates trigger cache invalidation, thereby improving performance.

For example, consider the following query:

{
  posts {
    nodes {
      id
      title
      tags {
        nodes {
          id
          name
        }
      }
    }
  }
  categories {
    nodes {
      id
      name
    }
  }
  tags {
    nodes {
      id
      name
    }
  }
}

This query retrieves a list of posts, along with all categories and tags. When this query is executed, the response includes the posts, categories, and tags that match the query as shown here:

The X-GraphQL-Keys header shows that the cached document is tagged with list:post, list:category, and list:tag. This tagging means that the cache will be invalidated whenever there’s a change in any of these entities—whether it’s a new post, category, or tag.

While this behavior ensures that your cache is up-to-date, it can lead to excessive cache invalidation. For instance, if a new tag is created and assigned to a post not included in this query, it will still trigger a purge('list:tag'), invalidating the cache for this query.

This means the cache could be cleared more often than you want for your specific use case, which could negatively impact performance.

Just A Note

Just a note, consider this query from the original article on this subject:

query GetPostsWithCategoriesAndTags {
  posts {
    nodes {
      id
      title
      categories {
        nodes {
          id
          name
        }
      }
      tags {
        nodes {
          id
          name
        }
      }
    }
  }
}

The WPGraphQL team changed things to only track list: types from the root.  So, if you run this query, your list of categories won’t be tracked because it is not at the root.

The Problem

The problem is that the list:category and list:tag keys could cause this document to be purged more frequently than you might like. WPGraphQL tracks precisely, but it doesn’t know your specific intention and what you care about.

For example, you might simply not care if this particular query is “fresh” when terms change. OR you might ONLY care for this query to be fresh when terms change. 

WPGraphQL doesn’t know the intent of the query, only what the query is.

Fortunately, you can customize the cache keys to better suit your specific needs, reducing unnecessary cache invalidations and improving performance.

Customizing Cache Keys

By customizing the cache keys, you can ensure that the cache is only invalidated when changes you believe are relevant to your use case occur. This involves fine-tuning the tags associated with your queries, allowing you to maintain optimal performance without sacrificing data accuracy.

Let’s do this by navigating to our WP admin and modifying the functions.php file.  Go to Appearance> Theme File Editor.  Select the functions.php file from your active theme.

Insert this code snippet at the bottom of your functions.php file to customize the cache keys for a specific GraphQL operation.  In this case, let’s add an operator name to the query we used in the section before.  We are calling our operation GetPostWithCategoriesAndTags:

add_filter( 'graphql_query_analyzer_graphql_keys', function( $graphql_keys, $return_keys ) {
    $keys_array = explode( ' ', $return_keys );
    if ( ! in_array( 'operation:GetPostsWithCategoriesAndTags', $keys_array, true ) ) {
        return $graphql_keys;
    }
    $keys_array = array_diff($keys_array, ['list:tag', 'list:category']);
    $graphql_keys['keys'] = implode( ' ', $keys_array );
    return $graphql_keys;
}, 10, 5 );

Code language: PHP (php)

You should have something that looks like this:

This snippet customizes the cache keys for the GetPostsWithCategoriesAndTags operation. It removes the list:tag and list:category keys from the cache, preventing their updates from invalidating the cache for this specific query. The array_diff() function is used to filter out the unwanted keys, and the modified keys are then reassembled into a string and returned.

Let’s test this now in WPGraphQL IDE and the browser dev tools:

Stoked!!!  Now as you see in the dev tools image, publishing new categories and tags, which triggers purge( 'list:category' ) and purge( 'list:tag' ) will not purge this document. 

We’re getting the benefits of cached GraphQL documents. The document is invalidated when the post is updated or deleted, but we’re letting the cache remain cached when categories or tags are created.

Conclusion

We hope you have a better understanding of using filters, as demonstrated above, to customize your cache tagging and invalidation strategies to better suit your project’s specific needs. By taking control of how cache keys are managed, you can optimize performance and reduce unnecessary cache invalidations.
As always, we look forward to hearing your feedback, thoughts, and projects so hit us up in our headless Discord!