Laravel • Varnish • Performance
by Dimitri König Reading time: ~4 minutes

Caching GraphQL queries with Varnish Cache

GraphQL queries are usually fetched via POST requests which Varnish does not cache per default. But there is a nice way to cache them anyway and make them blazingly fast.

With GraphQL queries you have a couple of issues to solve:

  • POST request with a query as a simple string in body content
  • Differentiate between cacheable READ operations (query) and dont-you-dare-cache WRITE operations (mutation)
  • Authorization via API Key (Bearer Token or some other Header)
  • Handling GraphQL errors

Checking for ideal conditions for GraphQL query caching

You need to exclude all WRITE operations, meaning no "mutation" queries. That means you need to check the POST request body for the word "mutation" (simple approach) and exclude such requests. Only simple ready query requests should be cacheable.

Luckily Varnish provides a VMOD called "bodyaccess" which is included in this VMODs bundle called varnish-modules.

The approach is to set a custom flag if all conditions are met. This flag will be used in some places further down the Varnish request flow:

1vcl 4.1;
2 
3# needed to check query content. get it here:
4# https://github.com/varnish/varnish-modules
5import bodyaccess;
6 
7sub vcl_recv {
8 # only cache GraphQL query if query is <10kb and no mutation query
9 if (req.url ~ "^/graphql"
10 && req.method == "POST"
11 && std.cache_req_body(10KB)
12 && bodyaccess.rematch_req_body("mutation") == 0
13 ) {
14 unset req.http.cookie;
15 
16 # set flag to use later
17 set req.http.isSimpleGraphQLQuery = true;
18 
19 return (hash);
20 }
21}

Extend cache key

Per default Varnish creates a cache key based on the requested url and the requested host or server ip. And it only caches GET requests:

1sub vcl_hash {
2 hash_data(req.url);
3 if (req.http.host) {
4 hash_data(req.http.host);
5 } else {
6 hash_data(server.ip);
7 }
8}

GraphQL queries are usually sent using http POST method, having the query as a string in its body content. And they may even contain a dedicated header for authorization (Authorization for Bearer Token or some other API key header).

For this to work you need to extend the cache key with the body content, and the authorization header, in case of a perfectly matching GraphQL query. This can look like this:

1sub vcl_hash {
2 # if GraphQL is cacheable, cache body AND api bearer token
3 # in case data is different depending on this token
4 if (req.http.isSimpleGraphQLQuery) {
5 bodyaccess.hash_req_body();
6 hash_data(req.http.Authorization);
7 }
8}

Quick tip: If you omit a return(operation) in any default vcl_* subroutine (like vcl_recv or vcl_hash, Varnish will continue with its default implementation. That way you can add additional business logic to it's default implementation.

Reset http request method back to POST

If you call return (hash) in Varnish, it transforms any http method to "GET", to fetch cacheable content from a defined backend. For caching POST requests, you need to reset it back to "POST", so that Varnish sends the actual POST request back to your backend.

1sub vcl_backend_fetch {
2 # set method back to POST since varnish sets it to "GET" per default
3 if (bereq.http.isSimpleGraphQLQuery) {
4 set bereq.method = "POST";
5 }
6}

Cache GraphQL response

And finally the caching part: if all conditions are met you can finally cache it. In this example you see a very low level TTL of 10s + 5s of grace time:

1sub vcl_backend_response {
2 # cache query for 10s+5s
3 if (bereq.http.isSimpleGraphQLQuery) {
4 set beresp.ttl = 10s;
5 set beresp.grace = 5s;
6 
7 unset beresp.http.set-cookie;
8 
9 return (deliver);
10 }
11}

How to handle errors

GraphQL always returns a 200 OK status code, even if there are schema, input or other errors. Such errors are usually within a top level "errors" property of the JSON response.

Since you are only caching READ queries, caching any errors could be fine too, depending on the cache duration and type of error.

Unfortunately the open source community edition of Varnish Cache does not have any response body inspection features. Right now you have 4 options:

  • The Enterprise edition has a module called xbody, which allows for such checks in response bodies.
  • You install the 3rdParty module libvmod-re from Uplex and use that to check the response body for a regex.
  • You write a vmod yourself.
  • You keep the TTL of a cached query low in case of any errors (which result from your backend and not a wrong a query) so that any wrongly cached content is not cached for long.

Stay up to date with my issue.dev Newsletter

Get notified when I publish a new article, and unsubscribe at any time.