At Platform.sh we recently came across a project that utilized GraphQL to pull in data for its front-end, but was experiencing performance issues with slow GraphQL queries. We found a way to conveniently cache those query results and greatly speed up the project as a result.
Queries to GraphQL are normally done as POST
requests, and caching the results of POST
requests is typically bad practice. In our use case, some queries are taking upwards of 5s to execute, which results in a rather poor user experience. Combining caching with a short TTL, (say, 60 seconds) the slightly-outdated content is preferable to an incredibly slow user experience.
We start at Fastly, and create a custom CDN endpoint to the GraphQL URI. This passes all requests (to that new endpoint) through the Fastly service, which uses Varnish to cache requests.
Fastly allows the user to upload a custom VCL, using the boilerplate VCL as a starting point. The only change we need to make is to ensure that POST
requests are treated in the same manner as GET
requests. This results in two small changes:
The block
if (req.method != "HEAD" && req.method != "GET" && req.method != "FASTLYPURGE") {
return(pass);
}
… should be changed to:
if (req.method != "HEAD" && req.method != "GET" && req.method != "POST" && req.method != "FASTLYPURGE") {
return(pass);
}
And similarly, the block
if ((beresp.status == 500 || beresp.status == 503) && req.restarts < 1 && (req.method == "GET" || req.method == "HEAD")) {
restart;
}
… changes to:
if ((beresp.status == 500 || beresp.status == 503) && req.restarts < 1 && (req.method == "GET" || req.method == "HEAD" || req.method == "POST")) {
restart;
}
These changes will allow POST
requests to be cached. However, you’ll note that
now all POST
requests to GraphQL will return the same data, regardless of the
query. This happens because the default cache_key
, which is based on the
request URL (req.url
) and some headers (req.http.Host
,
req.http.Authorization
, req.http.Fastly-SSL
) remains consistent between the
different query requests, and Fastly (correctly) returns the existing, cached
data for that key.
What we need, then, is a cache_key
that will change between different
queries.
The simplest way of accomplishing this is by adding the request body
(ie. the query) to the cache_key
. This can be done by simply adding req.body
to the cache_key
in Fastly’s settings.
However, the request body will be fairly long, and using such long strings in
the cache_key
is not ideal. Further, Fastly documentation for req.body
mentions that only the first 8kb of the body are available in the req.body
variable. This means that two different queries with identical content in the
first 8kb of the body may end up with the same cache_key
.
A better solution would be to generate a hash from the request body, include
that hash as a custom header in the POST
request, and then use the custom
header to create the cache_key
.
For example, if you were to include the hash in a header called body-hash
,
you could then add req.http.body-hash
to the cache_key
, and each unique
POST
request would return the correct data for that request, while also
caching the GraphQL response for as long as your configured TTL.