Surrogate keys
A surrogate key is a label on a cached response. The cache key identifies exactly one cached response; a surrogate key can sit on many of them. Purging a surrogate key removes every response that carries it, so the keys decide what an invalidation can reach.
Ways to control this
Section titled “Ways to control this”| Through | What it controls |
|---|---|
| Your origin | Keys of your own in the Surrogate- header. |
| Your GraphQL schema | @surrogateKey: key fields (of), fixed keys (static), and fields left out (ignore). |
gpilot.toml | Which scalars and field names count as key fields, and which types and fields are left out (automatic key fields). |
| The API, the CLI and the portal | Purging by key: gpilot purge <key>, purgeBySurrogateKey, or POST /purge with keys (purge on demand). |
Kinds of keys
Section titled “Kinds of keys”| Key | Example | Comes from | Plain HTTP | Purged by a write’s response |
|---|---|---|---|---|
| Cache key | ck:GET:/, ck:9f86d0 | The key the entry is stored under | Yes | No |
| Origin key | catalog-v3 | The Surrogate- header on your origin’s response | Yes | Yes, on a plain HTTP write and on a GraphQL mutation |
| Entity key | Product:id:p01 | A key field found in a GraphQL response | No | Yes, on a mutation |
| Empty-list key | ![Review] | A list in a GraphQL response that came back empty | No | Only as a target |
| Type key | Product | The types a GraphQL operation selects, whether or not any came back | No | No |
| Operation key | q:product, m:setPrice | The root field a GraphQL operation names | No | No |
| Static key | catalogue | @surrogateKey(static:) in your schema | No | No |
A key that is not purged by a write’s response is still a purge target: name it through the purge API or the CLI when you want that reach. What a GraphQL mutation purges on its own is the subject of automatic invalidation.
Reading the keys of one response
Section titled “Reading the keys of one response”With response.surrogate_key_header
switched on, every response lists its keys in gp-surrogate-key. A product page that asks for one
product, its category and its reviews comes back with:
gp-surrogate-key: ck:95f9f0f4…72b0 q:product Category Product Query Review catalogue Category:id:audio Product:category.id:audio Product:id:p01 Review:id:r01 Review:productId:p01| Key | Kind | A purge of it reaches |
|---|---|---|
ck:95f9f0f4…72b0 | Cache key | This query, with every variable set. |
q:product | Operation key | Every cached product(...) answer, whatever the id. |
Category, Product, Query, Review | Type keys | Every cached response that selected that type. |
catalogue | Static key, from @surrogateKey(static: "catalogue") | Everything the schema groups under it: every product and category. |
Category:id:audio, Product:id:p01, Review:id:r01 | Entity keys | Every cached response that contains that object. |
Product:category. | Entity key, from @surrogateKey(of: "category.id") | Every cached response that contains a product of the audio category. |
Review:productId:p01 | Entity key, from @surrogateKey(of: "productId") on Review | Every cached response that contains a review of p01. |
Purging Product:id:p01 does not reach Review:productId:p01: a purge matches keys exactly, type
name included. To clear a product and its reviews in one call, name both.
Keys on every entry
Section titled “Keys on every entry”purgeAll and gpilot purge --all remove every stored entry; there
is no key to name for that.
The cache key
Section titled “The cache key”Every stored response carries its own cache key as a surrogate key, with a ck: prefix, so one
response can be purged on its own:
- Plain HTTP:
ck:<METHOD>:<path>, the method and the path the request was sent to, query string included, for exampleck:GET:/products/42?color=blue. A request that carries a body adds#and a SHA-256 of the body. Scheme and hostname are not part of the key, so one path is one entry whether the client used the service hostname or a custom domain. An origin that answers the same path differently per hostname can put them back withhost = "include": the key is then the full URL, such asck:GET:https://catalog-api.k7m2qp.graphpilot.cloud/products/42?color=blue, and each hostname keeps its own entry. This is how you purge one plain HTTP response without a key of your own. There is no purge-by-URL call; you name the cache key, and the surest way to get its spelling is to copy it from the request detail in the portal or from thegp-surrogate-keyheader. - GraphQL:
ck:<queryHash>covers every variable set of a query, and a longer form that adds the operation name and the variables hash (ck:<queryHash>#<operationName>:<variablesHash>, the variables hash being 32 hex digits) addresses exactly one cached response. The variables hash here is a keyed digest that differs per service (see what the entry is keyed by), so copy it from the request detail in the portal or from request telemetry; it cannot be computed from the variables. Neither form carries the path or the query string the response was stored under, so a purge reaches it on every route: one purge ofck:<queryHash>clears that query on/graphql,/graphql/acmeand/graphql?locale=dealike.
A cache key becomes a surrogate key only when it is printable ASCII and at most 1024 bytes. A
normal URL is both; an entry stored under a longer one carries no ck: key, and only a key your
origin named or a purge-all reaches it. See purging one cached response by its cache
key.
Keys from your origin
Section titled “Keys from your origin”Your origin adds keys with a Surrogate-Key header on its response. On a read they tag the entry,
on a write they purge. The header and its format are on the Surrogate-Key
header.
Keys from your schema and data
Section titled “Keys from your schema and data”GraphQL only For plain HTTP, name keys in the Surrogate-Key header on your origin's response.
GraphPilot generates these keys from your schema and from the response data, without any
annotation. You can declare more with the @surrogateKey
directive.
Operation keys
Section titled “Operation keys”Every query response carries q:<field> for its root field, for example q:product. It lets you
purge a query regardless of which arguments were used. It is the field name the schema
defines, not an alias the client chose.
A mutation response carries m:<field>, for example m:setPrice. It describes the response,
so its root field is visible in telemetry and on the response. Nothing is ever stored under it,
because a mutation is never cached.
Type keys
Section titled “Type keys”Every type an operation selects adds a key with its name, such as Product, whether or not any
object of that type came back. An operation that returns an empty list still carries it.
Under a field of an interface or union type, the type selected is the interface or union, plus
each member the query names in a fragment: { media { title } } carries Medium and no Book,
whatever the response holds.
Entity keys
Section titled “Entity keys”<typename>:<keyField>:<keyValue> names one object. For a type Product whose id field is
p01, the key is Product:id:p01.
Without any directive, two rules decide which fields are key fields: a field whose type is a key
scalar (ID by default) and a field whose name is a key name (id, _id and key by default).
Both are configurable, and both can
be switched off where they pick a field that identifies nothing; see
ignore.
A key can combine several fields (PriceList:currency:EUR:region:de) or reach through a
related object (Product:category.id:audio). Both are entity keys. A composite key is only reached by a
purge naming it in full; see composite keys.
Empty-list keys
Section titled “Empty-list keys”A list that came back empty adds ![<typename>], so creating the first object of that type can
clear the cached “there are none” answer. See why the empty-list key
exists.
What a key may contain
Section titled “What a key may contain”Every key GraphPilot stores is printable ASCII (0x21 to 0x7E). How a key gets there
depends on where it comes from.
Keys from your schema and your data are folded
Section titled “Keys from your schema and your data are folded”This covers GraphQL keys built from field values, static keys from the directive, and keys
from your origin’s Surrogate-Key header. Every
character is folded into printable ASCII:
- Printable ASCII (
!through~) is kept exactly as written, casing included.:,{,-,_and every other printable symbol pass through unchanged. - Latin letters with a diacritic fold to their base letter:
MüllerbecomesMuller,ŁódźbecomesLodz,ÅngströmbecomesAngstrom. The decomposed spelling (a base letter followed by a combining mark) reaches the same key, because the combining mark is dropped. - Ligatures and similar letters take their conventional ASCII spelling:
ßbecomesss,æbecomesae,œbecomesoe,þbecomesth. - Every other character becomes one underscore: a space, a tab, a non-breaking space,
×, Cyrillic, CJK, emoji. A product titledBlue Shirtis taggedProduct:title:Blue_Shirt.
The folding is deliberately lossy. Blue Shirt and Blue_Shirt share a key, as do Müller and
Muller, and two Cyrillic words of the same length collapse onto the same run of underscores. A
collision means a purge clears a little more than you named, never less.
Casing is never changed, neither in your schema’s field names nor in the response data, so
Product:sku:AB1 and Product:sku:ab1 are two different keys.
Keys from your origin are folded the same way
Section titled “Keys from your origin are folded the same way”Keys in the Surrogate-Key header go through the same folding, so Brand:Müller from your
origin and Brand:Müller from your data reach the same key. ASCII spaces, tabs and commas
separate keys in the header, so none of them can be part of one. See the header’s
format.
A purge is folded the same way
Section titled “A purge is folded the same way”The purge API accepts any characters and folds them before it runs, so you
can purge with a value exactly as it appears in your data: Product:title:Blue Shirt reaches the
entry tagged Product:title:Blue_Shirt.
Keep keys to 1024 characters
Section titled “Keep keys to 1024 characters”A key your schema derives is never shortened. One that is longer than 1024 bytes after folding is still attached to the response, but a purge naming it is refused. (A cache key works differently: one that long is not attached at all, as above.)
Keep a key under 1024 bytes after folding: a few characters fold to two (ß becomes ss). Free
text such as titles makes a poor key in any case; prefer an identifier.
How many keys fit on one response
Section titled “How many keys fit on one response”The keys of one response share a budget of 16,000 bytes. GraphPilot stores them as short hashes to fit, and a response whose keys do not fit even then is not cached rather than cached with keys missing. The hash widths, how many keys each holds and what to do about a response that does not fit are on limits.
Related
Section titled “Related”- The Surrogate-Key header: keys from your origin
- Keys from the schema: declaring keys with
@surrogateKey - Debugging: reading the keys a response carries
- Purging through the API and CLI: naming keys in a purge