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.
What you need
Section titled “What you need”- A service whose list responses are cached (a
MISSthen aHITfor 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.
1. Pick one key for the list
Section titled “1. Pick one key for the list”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.
2. Tag the list
Section titled “2. Tag the list”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:
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 /postsHTTP/1.1 200 OKCache-Control: s-maxage=300Surrogate-Key: post-list3. 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 /postsHTTP/1.1 200 OKSurrogate-Key: post-listOn 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:
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:
gpilot purge --yes post-listOr call purgeBySurrogateKey; see purging through the API and CLI.
Check that it works
Section titled “Check that it works”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-listsend "$Q" # HITsend "$M" # the mutation; its response names post-listsend "$Q" # MISS: the list was purged, and the new post is in itFor 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.
Pitfalls
Section titled “Pitfalls”- 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
Booleanpurges 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
__typenamealone 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
nullto a value. Withauthor: Author @surrogateKey(of: "id")onBook, a book with no author is taggedBook:author.id:and the same book with authorA1is taggedBook: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
catalogkey on every list empties the whole catalog on each create. Watch the hit rate after you add a key.
Related
Section titled “Related”- Automatic invalidation: what a mutation purges on its own
- The Surrogate-Key header
- Keys from the schema
- Caching a REST API