Like in Spider-Man, the great power of headless WordPress is also a great responsibility. The extra control means we’re just as likely to build sites that are slower, not faster, than their WordPress counterparts.
There are many valuable tools for optimizing performance, but in this article, we’ll discuss WP GraphQL Smart Cache, specifically as it relates to leveraging Smart Cache Network Caching on WP Engine’s Managed Hosting for WordPress.
If you’re not familiar with how WP GraphQL Smart Cache works, I recommend reading the announcement post about its release. Understanding the basic concepts of tag-based cache invalidation, persisted queries, and network caching will be important.
General familiarity with Cache-Control
headers and HTTP Caching will also be valuable.
Table of Contents
Open Source Software
As we get started, I’d like to clarify quickly that WPGraphQL Smart Cache is not a product of WP Engine. It’s open-source software, maintained by a community of developers, in which WP Engine participates.
The Smart Cache plugin is also not solely responsible for caching your GraphQL responses. While it enables the needed features in WordPress and WPGraphQL, the actual caches and their integration with the plugin are dependent on hosting providers.
This means that the WPGraphQL caching experience described below is affected by the plugin, WP Engine’s integration, third-party cache providers (Cloudflare, Varnish), etc.
Oh, and remember how the two hardest things in software are caching and caching!
We at WP Engine would like to see this experience improved and will continue to advocate for and work towards improving it. But there are some things we don’t control. Even if we do, seemingly simple changes can affect caching for all of our customers, headless and traditional.
We hope the following helps you understand the current state of our platform and how you can control the caching experience to suit the needs of your headless application.
Opting Out
Quick review on leveraging the network cache. To enable network caching, the first and only completely required step is to change your GraphQL client from using POST requests to GET requests.
If you need to disable caching for specific queries, your best solution is always to switch those requests back to HTTP POST requests (this assumes you’re not using Smart Cache’s object caching feature).
If you’re using Faust.js, you can read the docs on the basics of using network caching.
Caching has layers, like an Ogre!
On the WP Engine platform, all requests go through a minimum of two layers of caching. However, events from WordPress, via Smart Cache, purge only one cache.
Client Cache
This could be the user’s browser or your headless application server. Whatever the source, these caches usually operate independently of Smart Cache and thus solely rely on the TTL and cache-control headers to determine caching.
CDN Cache
The next possible cache is WP Engine’s Advanced Network(AN), powered by Cloudflare. By default, this will not cache GraphQL responses. Cloudflare considers HTML and JSON responses dynamic, and thus uncachable, without additional configuration. To resolve this, WP Engine recently introduced the Edge Full Page Cache (EFPC) – a feature of AN that enables caching HTML and JSON responses.
Enabling EFPC means our GraphQL responses are now being cached on the edge. However, EFPC does not cache based on keys and is not purged by Smart Cache. It must rely on the configured TTL.
Server Cache
Third, all WP Engine WordPress sites have a Varnish server cache. Varnish is the only place (other than the object cache) where Smart Cache purges data. Varnish also uses the configured TTL as a fallback.
The fallback TTL is important for caches that don’t receive purge events. It’s also important because Smart Cache isn’t perfect. Settings, in particular, don’t yet purge cache but can affect GraphQL responses.
These are the only caches that affect the WordPress side of things. Your headless application might have its own GraphQL cache. Pages can also be cached by frameworks or Cloudflare based on the Cache-Control
header. The networks your site visitors are coming from may also have their own network caching layer. All of these are likely to rely on that configured TTL.
Cache | Caches GraphQL responses | TTL Config | Tag-based cache invalidation | Time-based cache invalidation |
---|---|---|---|---|
Browser | YES | Cache-Control | NO | YES |
Advanced Network | CONFIGURABLE | Cache-Control , CDN-Cache-Control | NO | YES |
Varnish | YES | Cache-Control | YES | YES |
Now that we (hopefully) understand the complex layers of cache involved and how they interact with smart cache, we can work towards an acceptable configuration of each of these layers.
The default scenario
Let’s work through a scenario. The site is on WP Engine, the GraphQL client is configured correctly, and we have default settings in Smart Cache. We’ll assume our three caching layers are the browser, Cloudflare, and Varnish.
Walkthrough
Layer | First request ever from Visitor A | Nth response from Visitor B |
---|---|---|
Browser | MISS | MISS |
Cloudflare | DYNAMIC | DYNAMIC |
Varnish | MISS | HIT: N |
Server | Responds | – |
Both visitors’ browsers don’t have a cache, so the response is sent to Cloudflare. Cloudflare responds with DYNAMIC
cause it’s considered uncacheable content (EFPC is not enabled yet). The first request misses Varnish, and the server responds. Visitor B’s request hits Varnish.
By default, Smart Cache with the help of Varnish will cache this response for 600 seconds (10 minutes) or until a tag-based purge event happens for the data, whichever comes first. Smart Cache settings can adjust this TTL.
If either of these visitors requests this again, before the 600s have elapsed, their browser caches will be used. If it’s past the 600s or a new user requests data after a purge event has happened, then the scenario starts from the beginning. Any new visitors will continue to receive hits from Varnish.
Improvements
This is a powerful and relatively simple caching config. This will help us scale significantly. Varnish response times are roughly an order of magnitude faster than the server (i.e., ~2s => ~200ms).
However, this configuration can be improved. The browser and Varnish TTL are the same. This means the browser cache will continue serving stale data for 600s even if Smart Cache invalidates the data in Varnish due to a purge event.
We could lower the TTL, but this would theoretically affect Varnish, lowering the overall cache hit rate. However, there’s a quirk in WP Engine’s Varnish config. It doesn’t let you set a TTL lower than 600s. You can set one higher, but not lower.
This actually helps us here. If we set our TTL to 60s, then quick page navigations will use the browser cache, but otherwise, Varnish will serve fresh data.
What if we want to raise the varnish TTL(better hit rates) while lowering the browser TTL? We’re out of luck. At this time, Smart Cache sets the header as max-age={TTL}, s-maxage={TTL}, must-revalidate
. If we could set s-maxage
differently, then we might be able to do this. Unfortunately, from the UI, this isn’t possible. This is possible with the graphql_response_headers_to_send
filter from PHP.
Solution
Another option would be to use WP Engine Web Rules. These rules allow us to add/change headers, which are applied after Varnish. This means we can configure a web rule to set a new Cache-Control
header that won’t affect Varnish.
This new header will be used by Cloudflare, the browser, and other miscellaneous caches.
This allows us to set max-age
or s-maxage
as we please. Our settings in Smart Cache only affect Varnish TTLs. Thus, those can now be raised while other caches can be lowered. This means more data in our cache, higher hit rates, and fewer stale cache responses. Fran would be so stoked; honestly, I am too!
The example above shows a Cache-Control
header that is the same for all GraphQL responses. Using the different headers Smart Cache sets and their values, it is possible to get very granular in setting different headers by post type, query, and more.
Edge Full Page Cache Scenario
For our next scenario, let’s add in EFPC. Same config we left off with.
Walkthrough
Layer | First request ever from Visitor A | Nth response from Visitor B |
---|---|---|
Browser | MISS | MISS |
Cloudfare | MISS | HIT |
Varnish | MISS | HIT: N |
Server | Responds | – |
Now that Cloudflare knows how to cache JSON with EFPC, we will start receiving HIT responses. Remember, EFPC fully relies on our TTLs to invalidate this data. But once the data goes stale will request and update from our server stack where Varnish can respond.
After 700 seconds, a visitor would get a stale
response from Cloudflare, but Varnish would respond with a HIT: N+1. Only once Varnish receives a purge event or the data expires would our request have to go all the way to the server. In the meantime, CF makes sure it stays fresh. Any additional Cloudflare points of presence can also be primed with data from the Varnish cache.
Improvements
This is a nice improvement, and we’ve reduced our latency by roughly another order of magnitude (i.e., ~200ms => ~20ms). With this config, we can control the browser and Cloudflare separately using max-age
and s-maxage
respectively. Awesome!
The final improvement we may choose to make is to control Cloudflare differently than any other downstream shared cache. Whether it’s a network cache we don’t control or something in our headless application, Cloudflare also respects alternative headers. You can opt to use CDN-Cache-Control
to target Cloudflare.
TL;DR
Here are the various subtle quirks of working with caches on the WP Smart Cache on the WP Engine platform.
Varnish
- Purged by events or TTL, whichever comes first.
cache-control
headers from WP get moved tox-orig-cache-control
and overridden- Sending requests with
x-wpe-no-cache: true
generally disables the Varnish cache. But doesn’t affect AN/EFPC. Varnish ignoresCache-Control: no-cache
directives - Varnish resets TTLs from Smart Cache below 600s to 600s.
- TTLs over 600 are respected, but the cache control header is modified to always be
max-age: {ttl}, must-revalidate
; everything else is stripped. 🙄 - Web rules can be used to modify headers, but this happens AFTER Varnish. For example, you can set the cache control headers you want, but Varnish still follows the TTLs from Smart Cache.
- Will not cache POST requests
- When a page is configured to be your “Home” page in WordPress, editing that page will purge the entire domain in Varnish.
EFPC
- Not Purged by events, only TTL.
Cache-Control
,CDN-Cache-Control
, andCloudflare-CDN-Cache-Control
headers can all be used to configure it. CF only uses the most specific of these headers, not a combination of all of them.- Cloudflare ignores the request directives of
Cache-Control: no-cache
to bypass the cache. - Will not cache POST requests
Conclusion
I hope you better understand the complexities of caching with WPGraphQL Smart Cache and the WP Engine Managed Hosting Platform for WordPress.
If you’re struggling with these workarounds or find any quirks we didn’t list here, please pass them along to support, your account rep, or find us in the Headless Discord.