Skip to content

Purging through the API and CLI

Plans Every plan Permission cache:purge

Most invalidation happens on its own, from a key your origin names on the response to a write or from what a GraphQL mutation returned. A purge you send is for everything else: a change that never went through GraphPilot, an admin tool, a support runbook, a nightly import, a migration.

You send it with gpilot purge, through the GraphPilot API (as a GraphQL mutation or with POST /purge), or with the Purge button on the service’s Purges page in the portal. The CLI is the quickest for a runbook or a CI job; the API is for your own tools and for the addressing modes the CLI does not offer.

AddressesCLIAPIReach for it when
One or more surrogate keysgpilot purge <key>...purgeBySurrogateKey, or POST /purge with keysYou know what changed. This is the normal one.
One cached response, by its cache keygpilot purge ck:<cacheKey>purgeBySurrogateKey, or POST /purge with keysOne response is wrong and it carries no key of your own.
The entry one telemetry request read or wroteNot availablepurgeByRequestYou are looking at a bad response in the portal’s request view and want exactly that one gone.
A GraphQL query, with or without one variable setNot availablepurgeByQueryHashYou know the operation is wrong but not which objects.
Everythinggpilot purge --allpurgeAll, or POST /purge with allA schema or config change made the whole cache untrustworthy.

Pick the narrowest one that reaches what you need. Everything you purge is an origin request somebody pays for on the way back in.

There is no purge by URL. To clear one plain HTTP response, purge its cache key, ck:<METHOD>:<path>, for example ck:GET:/products/42. The path carries its query string but no scheme or hostname, unless the service sets cache.http_key.host = "include", in which case the key is the full URL.

gpilot purge takes surrogate keys, cache keys included, and purges them in the current service:

Terminal window
gpilot purge Product:id:p01 Category:id:audio
gpilot purge ck:GET:/products/42
gpilot purge --soft product-42
gpilot purge --all

It shows what it is about to purge and asks you to confirm. In a script or CI job there is no one to ask, so the purge is refused unless you pass --yes (-y):

Terminal window
gpilot purge --yes --all

With no keys on the command line, it reads keys from stdin, one per line. That lets you keep a list of keys in a file, or purge what another tool produced:

Terminal window
gpilot purge --yes < keys.txt

--soft cannot be combined with --all. Use --service <subdomain> to purge a service other than the current one. The CLI sends the same purge as the API, with the credential of the active profile, so the same permission and limits apply. It has no form for a request id or a query hash; use the API or the portal for those. Every flag is in the command reference.

The API exposes purging as four mutations, each scoped to one service. Two of them, by key and everything, can also be sent over REST without writing GraphQL.

The GraphPilot API is one GraphQL endpoint:

https://api.graphpilot.io/graphql

Authenticate with a bearer credential. For a script or a CI job that is an API key you created in the portal; the CLI sends the same header with whatever credential the active profile holds.

Terminal window
curl -sS https://api.graphpilot.io/graphql \
-H "authorization: Bearer $GRAPHPILOT_API_KEY" \
-H 'content-type: application/json' \
-d '{"query":"mutation($i:PurgeBySurrogateKeyInput!){purgeBySurrogateKey(input:$i){purgedKeyCount}}","variables":{"i":{"serviceId":"shop-api","keys":["Product:id:p01"]}}}'

The organization comes from the API key or the login session. serviceId takes either the service’s id or its subdomain (the <subdomain> in https://<subdomain>.<label>.graphpilot.cloud, the value --service takes), and both are resolved within your organization. A service belonging to another organization is reported as not found rather than as forbidden, so an id cannot be used to probe for services you cannot see.

All four return the same type:

type PurgePayload {
success: Boolean!
purgedKeyCount: Int!
purgedKeys: [String!]!
}

purgedKeyCount counts keys, not cache objects. A key that matched nothing still counts, and no object count is available, because purges propagate between edge locations asynchronously. It can also be larger than the number of keys you sent: a GraphQL cache key sent without its ck: prefix is purged in both forms and counted twice, and purgedKeys lists both. See purging one cached response by its cache key.

It can be smaller, too. Duplicate keys are purged once: the API drops exact duplicates, and the edge then drops keys that are the same once folded, so Product:title:Blue Shirt and Product:title:Blue_Shirt in one call count as one, though purgedKeys lists both.

purgedKeys matters most for the indirect addressing modes: without it you cannot verify that a request id resolved to the entry you meant.

success is never false in a payload you receive. Every failure path returns a GraphQL error instead, so treat the presence of a payload as the success signal and read purgedKeys for what it covered.

mutation {
purgeBySurrogateKey(input: {
serviceId: "3fa3b8c2-9e41-4b7a-9c2e-1a6f4d8b2c10"
keys: ["Product:id:p01", "Category:id:audio"]
soft: true
}) {
success
purgedKeyCount
purgedKeys
}
}
FieldTypeNotes
serviceIdID!The service to purge in: its id or its subdomain.
keys[String!]!Cleartext surrogate keys. At most 1000 per request, each at most 1024 characters.
softBooleanMark entries stale rather than removing them.

Write each key as it appears in your data. GraphPilot folds a key into printable ASCII when it tags an entry, and folds a purge the same way, so Product:title:Blue Shirt reaches the entry tagged Product:title:Blue_Shirt. The folding rules are under what a key may contain.

Purging one cached response by its cache key

Section titled “Purging one cached response by its cache key”

A cache key counts as a surrogate key here, written with a ck: prefix. For plain HTTP it is ck:<METHOD>:<path>, the path the request was sent to with its query string, for example ck:GET:/products/42?color=blue. Scheme and hostname are left out, unless the service sets cache.http_key.host = "include": then the key is the full URL, ck:GET:https://catalog-api.k7m2qp.graphpilot.cloud/products/42?color=blue, and a path on its own names no entry. For GraphQL, ck:<queryHash> covers every variable set of a query, and the longer form in the portal’s request detail addresses one operation with one variable set. Neither names a path or a query string, so both reach the response on every route it was cached for. The portal shows the cache key with the prefix already in place, so you can copy it as it is. See the cache key.

GraphQL only

mutation {
purgeByQueryHash(input: {
serviceId: "3fa3b8c2-9e41-4b7a-9c2e-1a6f4d8b2c10"
queries: [
{ queryHash: "9f86d081884c7d65..." }
{
queryHash: "b1946ac92492d234..."
variablesHash: "4e07408562bed..."
operationName: "ProductPage"
}
]
}) {
success
purgedKeyCount
purgedKeys
}
}
FieldTypeNotes
queries[].queryHashString!Hash of the query document.
queries[].variablesHashStringWith it, exactly one cache object is purged. Without it, every variable set of that query is purged.
queries[].operationNameStringThe operationName the request sent, which is the name in its cache key. With variablesHash, a name purges that operation’s entry and null purges the entry of requests that sent no name. Omitted, GraphPilot looks the names up in your request telemetry (see below). Ignored without variablesHash.
softBooleanMark entries stale rather than removing them.

Omitting variablesHash is the useful case: one call removes every cached page, filter and sort order of the same query. Both hashes appear in your request telemetry, and the query hash is the same value a persisted query is registered under.

mutation {
purgeByRequest(input: {
serviceId: "3fa3b8c2-9e41-4b7a-9c2e-1a6f4d8b2c10"
requestIds: ["01J8XN3M7QK9V2ZC4T0GY5B6RD"]
}) {
success
purgedKeyCount
purgedKeys
}
}
FieldTypeNotes
serviceIdID!The service to purge in: its id or its subdomain.
requestIds[ID!]!Telemetry request ids. Each resolves to the cache key that request read or wrote.
softBooleanMark entries stale rather than removing them.

This one reads your stored request telemetry (see what Insights needs). A request that was not stored gives a request id nothing to resolve to.

mutation {
purgeAll(input: { serviceId: "3fa3b8c2-9e41-4b7a-9c2e-1a6f4d8b2c10" }) {
success
purgedKeyCount
purgedKeys
}
}

It takes no soft: a purge-all is always hard.

It addresses no surrogate key, so it returns purgedKeyCount: 0 and an empty purgedKeys. That is not a failure; it is what “everything” looks like when there are no keys to enumerate.

POST https://api.graphpilot.io/purge sends a purge without GraphQL. It runs purgeBySurrogateKey or purgeAll on your behalf, with the same credential, the same permission, the same limits and the same error codes, so everything above about keys, serviceId and the payload holds for it as well.

Terminal window
curl -sS https://api.graphpilot.io/purge \
-H "authorization: Bearer $GRAPHPILOT_API_KEY" \
-H 'content-type: application/json' \
-d '{"serviceId":"shop-api","keys":["Product:id:p01","Category:id:audio"]}'

The body is a JSON object in one of two forms:

BodyRunsNotes
{"serviceId": "...", "keys": [...], "soft": true}purgeBySurrogateKeysoft is optional and defaults to false.
{"serviceId": "...", "all": true}purgeAllNo soft: a purge-all is always hard.

serviceId is the service’s id or its subdomain, as for the mutations. The body has to match one form exactly. Anything else is refused with 400 and BAD_REQUEST before anything is purged: no serviceId, keys that is not a list of strings, keys and all together, soft with all, all with any value but true, or a field neither form names. There is no REST form of purgeByQueryHash or purgeByRequest; send those as mutations.

A purge that ran answers 200 with the PurgePayload as the whole body:

{ "success": true, "purgedKeyCount": 2, "purgedKeys": ["Product:id:p01", "Category:id:audio"] }

A refused one answers with a status that matches the code, and a body holding the same errors list the mutation returns, without locations and path:

{
"errors": [
{
"message": "Access denied: Cache",
"extensions": { "code": "FORBIDDEN" }
}
]
}
Statusextensions.code
400BAD_REQUEST: the body, or a key the purge refuses (an empty key, one over 1024 characters, more than 1000 keys, or one the edge refuses because it is over 1024 bytes once folded).
401UNAUTHENTICATED: no Authorization header, an sk_ value that is not a usable key (never issued, revoked, rotated or mistyped, not scoped to an organization, or holding only permissions an API key may not have), or a header that carries no Bearer credential. The response carries WWW-Authenticate. Any other bearer value that does not verify is refused by the edge in front of the API, also with 401. See when a credential is refused.
403FORBIDDEN (the credential lacks cache:purge), INSUFFICIENT_PERMISSIONS (a permission check in the database refused the request), ACCOUNT_LOCKED, or ACCOUNT_DELETION_IN_PROGRESS (the account or the organization is being deleted, and accepts no changes until the deletion is complete). Also FORBIDDEN when the service’s edge refused the purge: redeploy the service and try again.
404NOT_FOUND: no such service in your organization, or keys was an empty list.
409PRECONDITION_FAILED: the service has never been deployed or has no active hostname.
429TOO_MANY_REQUESTS: an sk_ key the API has not seen recently, sent over the validation budget for unrecognized keys. The key was not checked; the response carries Retry-After, so send the request again after that.
503SERVICE_UNAVAILABLE: an sk_ key could not be checked at that moment. It says nothing about the key; the response carries Retry-After: 5, so send the request again after that.
500INTERNAL_ERROR, and any other code. A partial purge is possible, as for the mutations: see permissions and errors.

Read extensions.code rather than the status where you branch: it is the value the mutation reports too, and the codes are listed once for both. Every answer carries Cache-Control: no-store. Like the mutations, the endpoint has no rate limit of its own beyond the API key validation budget.

A supplier’s price feed calls a small handler of yours whenever prices change. The handler writes the new prices to your database, so no request passes through GraphPilot and nothing is purged on its own. The handler then purges the products it changed:

Terminal window
curl -sS https://api.graphpilot.io/purge \
-H "authorization: Bearer $GRAPHPILOT_API_KEY" \
-H 'content-type: application/json' \
-d '{"serviceId":"shop-api","keys":["Product:id:p01","Product:id:p07"],"soft":true}'
{ "success": true, "purgedKeyCount": 2, "purgedKeys": ["Product:id:p01", "Product:id:p07"] }
  • Name the keys the entries carry. Product:id:p01 reaches every cached response that showed product p01, whatever query produced it. Read the spelling off the gp-surrogate-key header or the request detail in the portal rather than building it by hand: a key nothing was stored under still counts in purgedKeyCount.
  • soft suits a price feed. With a stale window on the entries, readers keep getting the old price for the moment it takes to fetch the new one, and your origin sees one refresh per entry rather than a burst. Without a stale window it purges like a hard one. See soft and hard.
  • Retry on 429, 503 and 500. Purging is idempotent, so sending the same body again is safe. Read Retry-After where the response carries it.
  • Give the handler its own API key, with the purge permission and nothing else. A purge sent with an API key shows no member under Actor on the Purges page; filter by Trigger Manual and the Key to find it.

soft: true marks matched objects stale rather than removing them. A stale object can still be served while GraphPilot revalidates it against your origin in the background, using the serve-stale window the entry was stored with, which comes from stale-while-revalidate on your origin’s Cache-Control header or from swr on @cacheControl.

An entry stored without such a window has none, so a soft purge on it behaves like a hard one. Soft purging is only meaningfully different if your origin sends stale-while-revalidate or your schema states an swr.

soft defaults to false on every mutation that accepts it, and purgeAll does not accept it.

Use a soft purge when a brief window of stale data is acceptable and you want to protect your origin from a stampede. Use a hard purge when the cached value is wrong rather than merely old.

On a service with shielding active, a purge reaches the edge locations and the shield in no fixed order, so an edge location purged before the shield could fetch the shield’s old copy again and keep it. To close that gap, GraphPilot sends every purge of a shielded service a second time with the same keys: a keyed purge about 2 seconds after the first, a purge of everything about 90 seconds after it. The payload you receive describes the first send; the second one runs on its own and never fails the call. Until it has run, an edge location can still answer with the old content.

A service counts as shielded when the configuration of its running deployment sets origin.shield and its plan includes shielding. Purges of any other service are sent once.

A call takes at most 1000 keys and each key at most 1024 characters; the mutations have no rate limit. An empty key is refused with BAD_REQUEST (gpilot purge drops blank lines from stdin before it sends anything). How the keys are counted is on limits.

The portal’s request view is part of Insights, on every plan. Its request detail shows a request’s surrogate keys and cache key on every plan; the cache key is also a column of the request list from Airliner (Business).

You needWhere it is
A surrogate keyThe request detail in the portal. The gp-surrogate-key response header, when switched on. See debugging.
A cache keyThe Cache key section of the request detail, prefix included. From telemetry through the API, the cache.key field, without the prefix: add ck: before you purge.
A query hash, variables hash or operation nameThe request detail in the portal, and gpilot pq list for registered operations.
A request idThe request view in the portal, or the gp-request-id response header when switched on.
A service idThe service’s subdomain works in its place. The id itself is in the portal.

Every purge mutation, and POST /purge, requires cache:purge, which service:edit does not include; see permissions. Failures come back as GraphQL errors with a code in extensions.code; every code is on codes. From a mutation they arrive in a 200 (from POST /purge with the status for the code), except a credential that is presented and refused, such as a revoked API key or one pasted without its prefix: that one is refused with 401 and UNAUTHENTICATED before the mutation runs. See when a credential is refused.