Skip to content

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.

ThroughWhat it controls
Your origin’s response headersHow long an entry lives (Cache-Control), whether it is shared or private, what it varies on (Vary), and the keys it can be purged by (Surrogate-Key).
Your GraphQL schemaLifetime and stale window (@cacheControl), who shares an entry (scopes), and purge keys (@surrogateKey).
gpilot.tomlWho a caller is and how the cache is partitioned ([cache.policy]), which headers and cookies reach the cache (filters), what a client’s own Cache-Control may do ([cache.request_directives]), and per-client cache rules (client profiles).
The API, the CLI and the portalRemoving entries before they expire: purge on demand.
The first block runs on every request, a cache hit included. The third runs only when the request goes on to your origin.

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.

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 includedRuns only when the request reaches the GraphQL layer (a miss or a pass)
The body size limit (request.max_body_bytes)The depth limit (request.max_query_depth)
AuthenticationThe document size limit (request.max_document_bytes)
Client profiles: version floor, body size cap, cache = "off", max_ageThe selection limit (request.max_selections)
The persisted query guardThe introspection setting (request.introspection)
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 400 and INVALID_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.

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.

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 errors array 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.

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.