Skip to content

Verifying a purge

A purge returns as soon as the edge has accepted it, and its result counts keys, not cached responses. To know what a purge actually covered, look it up in the service’s purge history. To know that a particular response is gone, request it again and read the gp-cache header.

Every purge of a service’s cache is recorded, as long as request telemetry is stored: the automatic ones a write’s response triggers, and the manual ones sent through the GraphPilot API, gpilot purge or the portal. Each record holds the cleartext keys the purge addressed.

The history is request telemetry, so it is stored and kept under the same rules; see what Insights needs.

Open the service and choose Purges. The list, its filters and the detail panel of one purge are described on Purges. A purge takes effect immediately, but it can take up to a minute to appear there.

The same history is the purges field on a Service, looked up here by its subdomain. It takes a required time window, an optional filter, and cursor pagination (first, at most 1000 per page, and after). Entries come newest first.

query {
services(condition: { subdomain: "my-shop" }) {
purges(
first: 50
timeRange: { from: "2026-09-22T00:00:00Z", to: "2026-09-23T00:00:00Z" }
filter: { trigger: [AUTO], key: "User:id:1" }
) {
nodes {
timestamp
trigger
soft
keys
keyCount
error
actor { name email }
triggeredByRequest { id path method }
}
pageInfo { hasNextPage endCursor }
}
}
}
Filter fieldTypeKeeps
trigger[PurgeTrigger!]AUTO, MANUAL, or both.
scopePurgeScopeEVERYTHING (purge-all) or KEYS.
softBooleantrue for soft purges, false for hard ones.
outcomePurgeOutcomeSUCCEEDED or FAILED (an error was recorded).
keyStringPurges with at least one key containing this text. % and _ are not wildcards.
actorUserIds[String!]Manual purges sent by these users. Automatic purges have no actor, so this filter excludes them. A purge sent with an API key is not recorded under a member’s user id.

All filter fields combine with AND; list fields match any of their values. keys is empty and keyCount is 0 for a purge-all. error is null unless the purge recorded one.

When a GraphQL mutation response carries keys that automatic invalidation follows, GraphPilot purges them and records one entry:

  • Trigger is automatic, and there is no actor.
  • Keys are the ones it purged: the entity keys and origin keys on the response, plus ![Type] for each type it touched. Keys that ride along without being purged (such as m:updateUser or a type key) are not listed.
  • Triggered by points to the mutation request.
  • It is always a hard purge.

A plain HTTP write whose response carries a Surrogate-Key header is recorded the same way, with the header’s keys and the write request as Triggered by.

A mutation whose response carried nothing to purge records nothing. If you expected a purge and the history shows none for that mutation, the response did not name the object; see what is not invalidated automatically.

The mutation request’s Overview tab in Insights also lists CACHE_PURGED under Edge codes for each automatic purge, with the number of keys (key_count) rather than the keys themselves. Edge codes are stored with the request, so they follow the same rules as the history.

The history tells you what was purged. To see that a particular response is really gone, request it before and after, and compare the gp-cache header, which is on every response by default.

  1. Send the query and check that it is cached. Run it twice if needed:

    Terminal window
    curl -sS -D- -o/dev/null "https://$GRAPHPILOT_HOST/graphql" \
    -H 'content-type: application/json' \
    -d '{"query":"{ user(id: 1) { id name } }"}' | grep -i '^gp-cache'
    gp-cache: HIT
    gp-cache-age: 412
  2. Purge it, for example with gpilot purge --yes User:id:1, or run the mutation that should invalidate it.

  3. Send the same query again. After a hard purge the entry is gone and the request goes to your origin:

    gp-cache: MISS

    After a soft purge of an entry stored with a stale window you see STALE instead: the old response is served once more while GraphPilot fetches a fresh one.

  4. Send it once more. It is a HIT again, with a small gp-cache-age, because the response from step 3 was stored as a new entry.

Purges reach the edge locations asynchronously. If the request in step 3 still reports a HIT with a large gp-cache-age, retry after a moment before looking further.

If it keeps hitting, the purge did not name a key that entry carries. Check, in this order:

  • The entry’s keys. Switch on the gp-surrogate-key header or open the request in the portal, and compare its keys with the ones in the purge record. See debugging.
  • A plain HTTP cache key without its prefix. A cache key taken from telemetry through the API needs ck: in front of it. A GraphQL cache key without the prefix is recognized and purged in both forms, so the prefix is not the cause there; a plain HTTP one sent without it matches nothing. See purging one cached response by its cache key.
  • A hostname in a plain HTTP cache key. By default the key has no scheme or hostname, so ck:GET:https://catalog-api.k7m2qp.graphpilot.cloud/products/42 matches nothing: purge ck:GET:/products/42. If the service sets cache.http_key.host = "include", it is the other way round: the key is the full URL, and each hostname the path was requested through has an entry of its own to purge.
  • A named operation purged by hash. purgeByQueryHash with a variablesHash and operationName: null purges only the entry of requests that sent no operation name. Pass the name from the request’s cache key, after the query hash and a # (graphql.operationName can differ for a request that sent none), or omit it to have the names looked up; the lookup refuses the purge when telemetry saw no request with that query and variable set.
  • A different service. A purge applies to one service; a request to another service’s hostname reads another cache.