Skip to content

The Surrogate-Key header

Your origin can name surrogate keys of its own in a Surrogate-Key response header. The same header does both halves of invalidation: on the response to a read it tags the cached entry, and on the response to a write it purges every entry carrying those keys. There is no API call to make, no credential to manage, and no second system that can be down when a write succeeds.

Which job the header does depends on the request that produced the response:

The response answersThe header
A plain HTTP GET or HEADTags the entry. Its keys are added to the ones every entry carries.
A GraphQL queryTags the entry. Its keys are added to the ones GraphPilot derives from your schema.
A plain HTTP POST, PUT, PATCH or DELETEPurges every cached entry carrying one of its keys.
A GraphQL mutationPurges every cached entry carrying one of its keys, together with what automatic invalidation purges.

The two halves fit together: tag a read with a key your origin chooses, then name that same key on the response to the write that changes it.

For a plain HTTP API:

# The response to GET /products/42: tag it
HTTP/1.1 200 OK
Cache-Control: max-age=3600
Surrogate-Key: product-42 catalog
# The response to PUT /products/42: clear it
HTTP/1.1 200 OK
Surrogate-Key: product-42

When the response to the PUT comes back through GraphPilot, every cached response tagged product-42 is purged, and the next GET /products/42 is a MISS.

For a GraphQL API it is the same, with a query and a mutation:

# The query response: tag it
HTTP/1.1 200 OK
Cache-Control: max-age=3600
Surrogate-Key: catalog-v3 promo-banner
# The mutation response: clear it
HTTP/1.1 200 OK
Surrogate-Key: catalog-v3

Every cached response tagged catalog-v3 is purged, whatever operation produced it and whichever fields it selected.

A purge from this header is always hard: the entry is removed, and the next request for it goes to your origin. It is recorded in the service’s purge history like any automatic purge; see verifying a purge.

The same holds for a purge named on the response to a plain HTTP write.

  • ASCII spaces, tabs and commas separate keys. A key cannot contain any of them.

  • Keys are folded into printable ASCII, the same way as keys from your schema: Müller becomes Muller, and a non-breaking space inside a key becomes _ rather than splitting it. A line that is not valid UTF-8 is read as Latin-1, so both spellings of Müller reach the same key. At most 1024 characters per key. See what a key may contain.

  • The header name is fixed at Surrogate-Key and cannot be changed. An origin that already uses that header for a CDN in front of GraphPilot sends the same keys to both.

  • Keys are not namespaced. A key you invent shares a space with the ones GraphPilot adds (ck:…, and for GraphQL Product:id:1, q:products, ![Product]). Prefix yours if you want to be sure they cannot collide.

  • Multiple headers merge. Per HTTP, several headers with the same name form one logical header. These two forms are equivalent, and GraphPilot accepts both:

    Surrogate-Key: product-123
    Surrogate-Key: category-456
    Surrogate-Key: product-123 category-456

GraphPilot removes your origin’s Surrogate-Key header before the response reaches the client. To see the keys a response carries, switch on the gp-surrogate-key header; see debugging.

The keys GraphPilot adds on its own cover what a response contains. This header covers what a write affects.

  • Any invalidation on a plain HTTP API. Without a schema, the only keys on an entry are its cache key and the ones your origin names. This header is how a write reaches the reads it changes.
  • A create that has to appear in a list. The new object’s key clears nothing, because no cached response contained it. A key naming the list does. See automatic invalidation.
  • A write that touches more than it returns. Recalculating an aggregate, expiring a sibling, cascading through a relation your response does not include.
  • Keys that only exist at runtime. A customer, a locale, a feature-flag cohort, a content version your CMS mints. Your origin knows them when it answers.
  • Keys from somewhere else entirely. A third-party API that returns its own cache tags can have them passed straight through.
  • It does not reach a response that was never tagged. Invalidation is a lookup by key, so a key that was never written on an entry matches nothing. If a purge appears to do nothing, check the entry’s keys first with the gp-surrogate-key response header.
  • It only runs on responses that pass through GraphPilot. A write made by a background job, an admin tool or a direct database change never produces a response here. Use the purge API or the CLI for those.
  • It does not confirm anything to your origin. The write’s response is the trigger; there is no acknowledgement of how many objects were removed. When request telemetry is stored (on every plan, within its logged-request allowance, and not in lean mode), the purge is recorded in the service’s purge history with the keys it covered.