As WP Engine approaches two years of offering solutions for headless WordPress on the Atlas Platform and Faust.js nears its 1.0 launch, we’ve had a lot of time to reflect and learn about the best practices needed to deliver headless WordPress solutions at scale. In this post, we’ll talk about one of those best practices, explore why it’s relevant, and talk about how we’re solving this for you on a foundational level so you can forget about this and go back to writing your site or app.
Understanding WordPress as a GraphQL API
While headless WordPress is becoming more widely used, it still represents a small fraction of total WordPress installs, and we work with people implementing headless WordPress across the spectrum of hosting providers. Because headless WordPress isn’t the default use case for many hosts, we need to think about how we structure our applications and tools to accommodate a wide variation in performance.
The largest bottleneck we typically see is around data fetching from the GraphQL API, particularly when you are building or prerendering a lot of pages all at once. I’ve personally seen examples where a Netlify build process was sending 300 requests per second to build a medium-sized website. 😮 For many shared WordPress hosts, that is easily enough load to start causing timeouts, which lead to failed builds or a
5XX error of some kind. With Atlas, we smartly scope your build cores to what your backend can support (since we know about both parts of your app), but that only solves part of the issue, and only on our platform 😉.
What if your site or application makes lots of simultaneous data requests in a hybrid rendering mode after build time? Or what if many of your pages can’t or shouldn’t be built statically because you need fresh data, like a newsroom or publication?
Making GraphQL Data More Cachable
Traditional WordPress hosting relies on full-page caching to both accelerate site resources and limit the load on shared infrastructure. For most hosts, WP Engine included, GET requests to the REST API are cached as well, typically along the same guidelines as a regular page cache: 600 seconds.
But most headless WordPress examples and starter projects, especially those using WPGraphQL, don’t take advantage of any caching layer because they use POST requests instead of GET requests.
In traditional HTTP-based systems, like REST APIs for example, request methods are almost like verbs, performing a certain action against a system. GET requests are typically used to retrieve data from an external system and are usually considered cachable requests.
POST requests, on the other hand, are requests that send data to the server, and these requests, because they often create resources, can cause side effects on the server. For this reason, POST requests bypass most network caching layers and generate a fresh response every time, even when the actual request is only retrieving data.
The POST Method as Convention
Part of this comes down to convention in how developers use GraphQL, with a POST request being the most common method for sending requests to a
/graphql endpoint. Even Apollo Client’s default fetch method is POST, for example.
POST requests to a GraphQL server typically result in a JSON-encoded object that looks like this as the request body:
But most GraphQL servers, WPGraphQL included, can also support GET requests without any additional configuration. The request values, like
operationName, and any
variables, just need to be encoded in a different manner. Instead of sending the details of our query in the request body as JSON, they can be encoded in query parameters like many other GET requests.
Switching to GET Requests
In looking at this example taken from the GraphQL docs, the following query results in a URL like the one below:
And for the developer, switching to GET requests by default using an Apollo
HTTP Link is as simple as switching the
useGETForQueries option to
true. The Apollo client will then handle constructing your query’s URL for you. In doing this, you can take advantage of any network caching your host may have applied to this path, which should speed up your sites and make them more resilient. In this case, Apollo only uses GET for queries and will continue to use POST for mutations.
A Change in Recommended Practice
After working with loads of developers and site builders across a number of platforms, the Faust.js and WPGraphQL teams felt like it was time to change the recommended practice to using GET requests by default, and you’ll see that change implemented as a new default in Faust.js soon.
We also strongly recommend that you explore WPGraphQL Smart Cache. Even if you decide not to use GET requests, WPGraphQL Smart Cache will accelerate your post requests using an object cache, providing you with some performance benefits.
With that in mind, there are a few limitations to GET requests that should be called out.
Limitation 1: Query URI Can Be Too Long
Since a GET request contains your entire GraphQL query and variables, if it’s rather complex, it may bump into a server character limit. You can see an example of that here in this GitHub issue noting an error code Request-URI Too Long. For most sites, you should be fine, but if you have long queries and need to use GET requests, then the persisted queries feature of WP GraphQL Smart Cache should help you out.
Per the docs:
In the GraphQL ecosystem, the term “Persisted Queries” refers to the idea that a GraphQL Query Document can be “persisted” on the server, and can be referenced via a Query ID.
There are several advantages to using Persisted Queries:
- Avoid query string limits
- Reduced upload cost per request (uploading large GraphQL query documents for each request has a cost)
- Ability to mark specific queries as allow/deny
- Ability to customize the cache-expiration time per query
- Future customizations at the individual query level
WPGraphQL Smart Cache provides support for “Persisted Queries” and plays nice with the Apollo Persisted Query Link.WPGraphQL Smart Cache Docs, Persisted Queries
As you can see, there are lots of benefits to using persisted queries that may address other caching and access control issues. The newest version of Faust.js makes it easy to enable this feature using Apollo’s persisted queries link functionality. To opt-in to persisted queries in Faust, set the value of the
usePersistedQueries option in
Limitation 2: You Need Fresh Data
In most network caching strategies, server responses are cached for a particular length of time, for example, 600 seconds. If a change happens to the content within that 600-second window, it may not be reflected on your site because the data you receive from your query is cached. Typically there are ways to manually clear your site’s cache, but may not be something you want content creators to do.
WPGraphQL Smart Cache also provides fine-grained cache invalidation based on WordPress events, so that any of your cached queries are invalidated when the underlying data changes.
Per the docs:
Unlike RESTful APIs where each enpdoint is related to specific resource type, GraphQL Queries can be constructed in nearly infinite ways, and can contain resources of many types.
Because of the flexibility that GraphQL offers, caching and invalidating caches can be tricky.
This is where WPGraphQL Smart Cache really shines.
When a GraphQL request is executed against the WPGraphQL endpoint, the query is analyzed at run time to determine:
- The operation name of the query
- The ID of the query (a hash of the query document string)
- What types of nodes were asked for as a list
- The individual nodes resolved by the query
The results of the GraphQL query are cached and “tagged” with this meta data.
When relevant events occur in WordPress, WPGraphQL Smart Cache emits aWPGraphQL Smart Cache Docs, Cache Invalidation
purgeaction to purge cached documents with the key(s) called in the purge action.
As you can see with some of these feature descriptions, the tooling around WPGraphQL has grown substantially over the last year. As a result of WP Engine’s continued investment in the ecosystem and the work being done by agencies building on the Atlas platform, headless WordPress is becoming easier to manage at an enterprise scale.
What Does This Mean for You?
If you are currently using Faust.js, in the version 1.0 release you will see the default data-fetching method shift from POST to GET methods for queries. As a result, you should do the following things before upgrading:
- Think about the caching strategy for your application: how often are your pages refreshed? What data can be cached and for how long?
- Explore WPGraphQL Smart Cache and its features. WP Engine supports it out of the box, but there are methods to add support for your hosting provider
- Test the newest version before upgrading: do all your queries work and can you render all of your routes?
For simple sites, this change in the default fetch method shouldn’t do anything but make your builds faster.
For more complex sites or sites using complex queries, you may need to do additional testing to implement these patterns, but you are more likely to see performance gains by implementing these steps than a smaller site.
And just because the Faust.js framework is making this change to the default method based on data from production usage, optional adoption is important to that team. If you would like to switch back to POST requests, you can do so by setting the
useGETForQueries property to
false in the
If you need to make one-off POST requests, you can pass some additional
Need Guidance or Have Feedback?
If you need any help or want to talk through how these changes may impact your application, join us in the Headless WP Discord so we can get your feedback.