Skip to content

Cache invalidation

A cached response is only useful while it is still true. GraphPilot removes an entry when the data behind it changes, so you can give responses a long freshness lifetime without serving old data after a write.

Invalidation works in two steps, the same for a plain HTTP API and a GraphQL API: tag, then purge. Every cached response is tagged with surrogate keys, labels that name what it contains. A purge names keys, and every entry carrying one of them is removed, whichever request produced it.

ThroughWhat it controls
Your originThe Surrogate-Key header: tags a read, purges on a write.
Your GraphQL schema@surrogateKey on types and fields; a mutation purges what it returned automatically.
gpilot.tomlWhich fields count as keys and how far a mutation reaches ([surrogate_key]).
The API, the CLI and the portalPurge on demand: gpilot purge, the purge mutations and POST /purge, and the Purges page.

Every entry carries its own cache key (ck:…), so one response can always be purged on its own. On top of that:

  • Your origin names keys in a Surrogate-Key header on the response to a read (a GET or HEAD, or a GraphQL query). Use it for anything that identifies what the response holds: a product, a customer, a content version.
  • For GraphQL, GraphPilot derives keys from your schema and the response data: one key per object it returned (Product:id:p01), one per type and operation, and any you declare with @surrogateKey. Clients do not have to select the fields a key needs; selection set expansion adds them.

A purge happens in one of three ways.

Your origin answersGraphPilot purges
A plain HTTP POST, PUT, PATCH or DELETEEvery key in the Surrogate-Key header on the response.
A GraphQL mutationEvery object the mutation returned, automatically, plus every key in the Surrogate-Key header on its response.

For a plain HTTP API this is the whole mechanism: tag the GET with a key, name the same key on the response to the write, and the next GET is a MISS. See the Surrogate-Key header.

For GraphQL most of it needs no code: a mutation that returns the object it changed clears every cached response holding that object. It leaves some cases uncovered, most importantly a new item in a cached list that was not empty. Read automatic invalidation before you rely on it.

For a change that never passes through GraphPilot (a database migration, a nightly import, an admin tool), purge through the GraphPilot API, with gpilot purge, or from the portal. You can address surrogate keys, one cached response by its cache key, a request from your telemetry, a GraphQL query by its hash, or the whole cache. See purging through the API and CLI, and verifying a purge to confirm what it removed.

Every entry has a freshness lifetime from your origin’s Cache-Control header or, for GraphQL, your schema’s @cacheControl policy. When it runs out, the entry stops being served whether or not anything purged it. That bounds how long an entry can be wrong in the worst case, but it is not your invalidation strategy.

The pages in this section use one running example: a product catalog with Product, Category and Review types and a setPrice mutation, served by a service called shop-api. Product carries @surrogateKey(of: "id"), @surrogateKey(of: "category.id") and @surrogateKey(static: "catalogue"), so every product can be purged on its own, with its whole category, or with the whole catalog.

Tag. A product page asks for one product, its category and its reviews. With the gp-surrogate-key header switched on, the response shows every key the entry carries:

Terminal window
curl -sS -D- -o /dev/null "https://$GRAPHPILOT_HOST/graphql" \
-H 'content-type: application/json' \
-d '{"query":"query ProductPage { product(id: \"p01\") { id name price category { id name } reviews { id rating } } }"}'
gp-cache: MISS
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

Purge on a write. A setPrice(id: "p01", price: 2450) mutation returns the product it changed, so its response carries Product:id:p01 and GraphPilot purges it on the way back. The next ProductPage request is a MISS with the new price. The full walk-through, with what Insights and the Purges page show, is on automatic invalidation.

Purge you send. A nightly price import writes to the database directly, so no response passes through GraphPilot. The import job purges what it changed itself:

Terminal window
gpilot purge --yes --service shop-api Product:id:p01 Product:id:p07

The same purge from a webhook, without the CLI, is under purging over REST.