How a request flows
Every request goes through the same three blocks. Knowing which block a check sits in answers most “does X apply on a cache hit” questions on its own.
Ways to control caching
Section titled “Ways to control caching”| Through | What it controls |
|---|---|
| Your origin’s response headers | How long an entry lives (Cache-), whether it is shared or private, what it varies on (Vary), and the keys it can be purged by (Surrogate-). |
| Your GraphQL schema | Lifetime and stale window (@cacheControl), who shares an entry (scopes), and purge keys (@surrogateKey). |
gpilot.toml | Who a caller is and how the cache is partitioned ([cache.), which headers and cookies reach the cache (filters), what a client’s own Cache- may do ([cache.), and per-client cache rules (client profiles). |
| The API, the CLI and the portal | Removing entries before they expire: purge on demand. |
Three blocks
Section titled “Three blocks”- Every request. The request body size limit is applied, credentials are verified, a client profile is matched, and the persisted query guard rejects what the mode does not allow.
- Cache. The request is answered from the cache, or it goes on. What decides this is on when a response is cached.
- Only on a miss or pass. The GraphQL document is parsed and checked against the GraphQL limits, trusted mode refuses an unregistered operation, the request is signed and sent to your origin, and a failed origin request is retried. A mutation’s response then purges what it named.
Authentication runs on every request
Section titled “Authentication runs on every request”Authentication sits above the cache, so a credential is verified whether the answer comes from the cache or from your origin. A caller with an expired or forged token is refused at the edge, and never receives a cached response.
This is also why cache partitioning can use claims at all: the claims a partition is derived from have already been verified.
The GraphQL layer runs only on a miss
Section titled “The GraphQL layer runs only on a miss”Parsing, planning and selection set expansion sit below the cache. A hit does not parse the document, which is where most of the latency saving comes from.
| Runs on every request, hit included | Runs only when the request reaches the GraphQL layer (a miss or a pass) |
|---|---|
The body size limit (request.) | The depth limit (request.) |
| Authentication | The document size limit (request.) |
Client profiles: version floor, body size cap, cache = "off", max_age | The selection limit (request.) |
| The persisted query guard | The introspection setting (request.) |
| The refusal of an unregistered operation in trusted mode |
Consequences worth knowing:
- A cached response is never re-checked. A document that was under every limit when it was stored keeps being served from the cache if you tighten a limit later, until the entry expires or is purged. Refusals themselves are never cached.
- Query validation happens at your origin, not at the edge. GraphPilot parses and plans;
it does not validate the operation against your schema. Two exceptions: a document GraphPilot
cannot read unambiguously (one that does not parse, names an operation it lacks, or spreads
fragments in a cycle) is refused with
400andINVALID_GRAPHQL_REQUEST, and a query that selects a field your deployed schema does not declare is forwarded but its answer is never stored. - A cached response is replayed, not regenerated. It is served as stored.
Trusted mode is enforced on misses
Section titled “Trusted mode is enforced on misses”The refusal of an unregistered operation sits below the cache, so it runs on a miss and not on
a hit. A response cached while an operation was still permitted stays reachable after you switch
to trusted mode, until it expires or is purged: the cache is keyed by the query hash, and nothing
in that key changes with the mode.
gpilot pq rm deletes the registry entry and purges the cache key for it, which is why
clearing what your clients registered is part of switching modes. See persisted
queries.
The headers GraphPilot adds to a response, and the ones it removes from your origin’s, are listed in HTTP headers. With shielding active, a miss at an edge location is sent on to the shield, which runs the same blocks with its own cache.
What is not in the path
Section titled “What is not in the path”Stating the absences, because they are what an evaluation actually turns on:
- No conditional revalidation. GraphPilot never sends a conditional request and never
handles a
304. A background revalidation is a full request. - No change to what your response says.
The data your origin produced is the data the client receives. A plain HTTP body is passed
through byte for byte, on a hit and on a miss. A GraphQL body is not re-serialized: GraphPilot
decodes a compressed body and cuts out only what it has to (the fields it injected, an error
on one of those fields, and an
errorsarray that this leaves empty), keeping your origin’s key order and number formatting. In the rare case a cut cannot be made in place, the edited response is serialized again, and its formatting can then differ while its content does not. Nothing else about the content changes. - No query rewriting beyond selection set expansion, which adds key fields and removes them again before responding.
- No composition of subgraphs. One service is one origin. To cache a federated graph, put GraphPilot in front of your federation router, on any plan: see federation routers.
- No rate limiting and no request cap. See request limits.
- No schema validation of client operations at the edge. Your origin remains the authority.
Where a WebSocket leaves the path
Section titled “Where a WebSocket leaves the path”A subscription handshake is an ordinary HTTP request and goes through all three blocks, so the body size limit, authentication and signing all apply to it. It is never retried. Once it is upgraded, the connection is a tunnel: nothing on this page applies to the messages that follow, and your origin is on its own for them.