Skip to content

Lists and creates

An update or a delete clears the cached lists that contained the object, because the object’s key is on both. A create cannot: the new object is in no cached response yet, so its key clears nothing, and a list that already held items keeps showing the old ones until it expires. Why that is so is on automatic invalidation. This guide gives the list a key that the create names, so the create purges it.

The running example is a blog. posts lists the latest posts and createPost adds one. As plain HTTP, the same is GET /posts and POST /posts.

  • A service whose list responses are cached (a MISS then a HIT for the list).
  • For GraphQL, the schema you deploy, and an origin that can set a response header on the mutation. For plain HTTP, an origin that can set response headers on the read and the write.

Choose a key that names the list, not an object: post-list. It has to be printable ASCII with no spaces or commas; see what a key may contain. Prefix it if you want to be sure it never collides with a key GraphPilot adds.

Pick the width deliberately. One key on every list of posts purges all of them on every create, which is usually right for a blog. A key per author (post-list-7) purges only that author’s lists, at the cost of your origin knowing the author on the write.

GraphQL. Put a static key on the list field. Every response that selects posts then carries post-list, whatever came back, an empty list included:

schema.graphql
type Query {
posts(first: Int = 20): [Post!]! @surrogateKey(static: "post-list")
}

Put the key on the Post type instead if every query that touches a post should be purged together. The difference is which queries carry it; see a group you purge by hand. Your origin can also tag the query response itself with a Surrogate-Key: post-list header; both kinds of key are reached by the same purge.

Plain HTTP. Your origin tags the response to the read:

# The response to GET /posts
HTTP/1.1 200 OK
Cache-Control: s-maxage=300
Surrogate-Key: post-list

3. Name the key on the create’s response

Section titled “3. Name the key on the create’s response”

A static key is never purged automatically, so the write has to name it. Set the header on the response to the create, in the resolver or a middleware that knows the operation succeeded:

# The response to the createPost mutation, or to POST /posts
HTTP/1.1 200 OK
Surrogate-Key: post-list

On a GraphQL mutation, and on the response to a plain HTTP POST, PUT, PATCH or DELETE, this header purges every cached response carrying post-list, at every edge location. See the Surrogate-Key header.

Send it only on success. A failed create changed nothing, and a purge it triggers costs origin requests for nothing.

Another way for GraphQL: return the parent. When the list hangs off an object, such as author { posts }, let the mutation return that object. Its key is purged automatically and reaches every cached response that contained it, the list included:

schema.graphql
type CreatePostPayload {
post: Post
author: Author # purges Author:id:<id>, so author { posts } is refetched
}

Selecting the payload’s fields is not required from the client; see mutations that return a payload type.

4. Purge from outside, when the create does not pass through

Section titled “4. Purge from outside, when the create does not pass through”

A post created by an import job or an admin tool never produces a response here. Purge the key from that code:

Terminal window
gpilot purge --yes post-list

Or call purgeBySurrogateKey; see purging through the API and CLI.

Terminal window
Q='{"query":"{ posts { id title } }"}'
M='{"query":"mutation { createPost(title: \"Hello\") { post { id } } }"}'
send() { curl -sS -D- -o/dev/null "https://$GRAPHPILOT_HOST/graphql" \
-H 'content-type: application/json' -d "$1" | grep -i '^gp-cache:'; }
send "$Q" # MISS: stored, tagged post-list
send "$Q" # HIT
send "$M" # the mutation; its response names post-list
send "$Q" # MISS: the list was purged, and the new post is in it

For plain HTTP, the same with GET /posts and POST /posts. To see that the list carries the key, switch on gp-surrogate-key; see debugging. The service’s Purges page lists the purge with the request that triggered it; see verifying a purge.

  • An empty list needs nothing of this. A cached empty list is tagged ![Post], which any mutation touching a post purges. Only a list that already held items needs a key; see why the empty-list key exists.
  • A mutation that returns Boolean purges nothing automatically. It carries no object and no key. Return the object or its parent, or name the key in the header.
  • A selection of __typename alone still carries keys. { posts { __typename } } reports how many posts exist, which a create changes, so it is tagged like a full selection and purged with it. Only { __typename } at the root, which resolves no field, earns no content keys.
  • A nullable key field changes its key when it goes from null to a value. With author: Author @surrogateKey(of: "id") on Book, a book with no author is tagged Book:author.id: and the same book with author A1 is tagged Book:author.id:A1. Purging only the new key misses the cached version without an author. Purge by the object’s own key instead, Book:id:B1, which reaches it whatever its author was.
  • A key that names too much purges too much. One catalog key on every list empties the whole catalog on each create. Watch the hit rate after you add a key.