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.
Which one to use
Section titled “Which one to use”| Addresses | CLI | API | Reach for it when |
|---|---|---|---|
| One or more surrogate keys | gpilot purge <key>... | purgeBySurrogateKey, or POST /purge with keys | You know what changed. This is the normal one. |
| One cached response, by its cache key | gpilot purge ck:<cacheKey> | purgeBySurrogateKey, or POST /purge with keys | One response is wrong and it carries no key of your own. |
| The entry one telemetry request read or wrote | Not available | purgeByRequest | You 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 set | Not available | purgeByQueryHash | You know the operation is wrong but not which objects. |
| Everything | gpilot purge --all | purgeAll, or POST /purge with all | A 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.
From the CLI
Section titled “From the CLI”gpilot purge takes surrogate keys, cache keys included, and purges them in the current service:
gpilot purge Product:id:p01 Category:id:audiogpilot purge ck:GET:/products/42gpilot purge --soft product-42gpilot purge --allIt 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):
gpilot purge --yes --allWith 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:
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.
Through the GraphPilot API
Section titled “Through the GraphPilot API”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.
Calling it
Section titled “Calling it”The GraphPilot API is one GraphQL endpoint:
https://api.graphpilot.io/graphqlAuthenticate 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.
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.
The shared payload
Section titled “The shared payload”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.
purgeBySurrogateKey
Section titled “purgeBySurrogateKey”mutation { purgeBySurrogateKey(input: { serviceId: "3fa3b8c2-9e41-4b7a-9c2e-1a6f4d8b2c10" keys: ["Product:id:p01", "Category:id:audio"] soft: true }) { success purgedKeyCount purgedKeys }}| Field | Type | Notes |
|---|---|---|
serviceId | ID! | 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. |
soft | Boolean | Mark 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.
purgeByQueryHash
Section titled “purgeByQueryHash”GraphQL only
mutation { purgeByQueryHash(input: { serviceId: "3fa3b8c2-9e41-4b7a-9c2e-1a6f4d8b2c10" queries: [ { queryHash: "9f86d081884c7d65..." } { queryHash: "b1946ac92492d234..." variablesHash: "4e07408562bed..." operationName: "ProductPage" } ] }) { success purgedKeyCount purgedKeys }}| Field | Type | Notes |
|---|---|---|
queries[]. | String! | Hash of the query document. |
queries[]. | String | With it, exactly one cache object is purged. Without it, every variable set of that query is purged. |
queries[]. | String | The 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. |
soft | Boolean | Mark 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.
purgeByRequest
Section titled “purgeByRequest”mutation { purgeByRequest(input: { serviceId: "3fa3b8c2-9e41-4b7a-9c2e-1a6f4d8b2c10" requestIds: ["01J8XN3M7QK9V2ZC4T0GY5B6RD"] }) { success purgedKeyCount purgedKeys }}| Field | Type | Notes |
|---|---|---|
serviceId | ID! | 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. |
soft | Boolean | Mark 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.
purgeAll
Section titled “purgeAll”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.
Over REST
Section titled “Over REST”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.
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:
| Body | Runs | Notes |
|---|---|---|
{"serviceId": ". | purgeBySurrogateKey | soft is optional and defaults to false. |
{"serviceId": ". | purgeAll | No 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" } } ]}| Status | extensions. |
|---|---|
400 | BAD_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). |
401 | UNAUTHENTICATED: 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-. 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. |
403 | FORBIDDEN (the credential lacks cache:purge), INSUFFICIENT_ (a permission check in the database refused the request), ACCOUNT_, or ACCOUNT_ (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. |
404 | NOT_FOUND: no such service in your organization, or keys was an empty list. |
409 | PRECONDITION_: the service has never been deployed or has no active hostname. |
429 | TOO_: 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. |
503 | SERVICE_: 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. |
500 | INTERNAL_, 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.
Example: a price-feed webhook
Section titled “Example: a price-feed webhook”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:
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:p01reaches every cached response that showed productp01, whatever query produced it. Read the spelling off thegp-surrogate-keyheader or the request detail in the portal rather than building it by hand: a key nothing was stored under still counts inpurgedKeyCount. softsuits 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,503and500. Purging is idempotent, so sending the same body again is safe. ReadRetry-Afterwhere 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 and hard
Section titled “Soft and hard”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.
With shielding
Section titled “With shielding”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.
Limits
Section titled “Limits”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.
Finding the values
Section titled “Finding the values”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 need | Where it is |
|---|---|
| A surrogate key | The request detail in the portal. The gp- response header, when switched on. See debugging. |
| A cache key | The 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 name | The request detail in the portal, and gpilot pq list for registered operations. |
| A request id | The request view in the portal, or the gp- response header when switched on. |
| A service id | The service’s subdomain works in its place. The id itself is in the portal. |
Permissions and errors
Section titled “Permissions and errors”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.
Related
Section titled “Related”- Verifying a purge: the purge history and how to confirm a purge worked
- The Surrogate-Key header: purging from your origin on a write
- Automatic invalidation: what a GraphQL mutation purges without a call
- Surrogate keys: what you can address
- API keys: creating a key with
cache:purge