Skip to content

Downstream header

GraphQL only A plain HTTP response reaches the client with your origin's Cache-Control unchanged.

The precedence rule decides whether GraphPilot stores a response in its own cache. Separately, cache.policy.downstream decides whether GraphPilot writes a Cache-Control header derived from your schema on the response it sends back to the client, similar to what Apollo Server’s cache-control plugin does.

The setting only ever synthesizes a header for a response your schema’s @cacheControl policy decided. It never rewrites, removes or replaces a Cache-Control header your origin sent:

  • Your origin sent Cache-Control. The client receives that header unchanged, in every mode, off included. A private, max-age=60 from your origin reaches the client as private, max-age=60.
  • Your origin sent none, and the schema decided. This is the case the setting is for. With off, the client gets no Cache-Control. With if_cacheable or always, GraphPilot writes one from the schema policy, as described below.

These are independent of what GraphPilot stores. It can cache a schema-decided response without telling the client, or a downstream CDN, anything about it, and the header it writes does not by itself change what GraphPilot stores.

gpilot.toml
[cache.policy]
downstream = "off" # "off" | "if_cacheable" | "always"

GraphPilot never synthesizes a Cache-Control header. An origin header still passes through unchanged. This is the default so that a service without the setting keeps sending exactly what its origin sent.

GraphPilot writes a Cache-Control header for a schema-decided response only when the schema policy is actually cacheable, that is, it has a positive maxAge. A Cache-Control header from your origin is forwarded as it is, as in every mode.

GraphPilot writes a Cache-Control header whenever a schema policy resolved, even when that policy is not cacheable. If no policy resolved, no header is written, in always mode either: there is nothing to reflect, so GraphPilot never fabricates a public guess.

A query that selects no field carrying a @cacheControl hint, directly or through its return type, resolves no schema policy at all. Neither mode writes a header for it. The client then sees only what your origin sent, if anything.

A query that selects a field your deployed schema does not declare resolves a policy with a maxAge of 0 and no stale window, however long its other fields could be kept (see a field the deployed schema does not declare). if_cacheable writes no header for it, and always reflects that 0, so the header never advertises a lifetime GraphPilot itself did not store the response for.

For a response your schema’s policy decided, the header never claims to be more permissive than the policy GraphPilot used for its own storage decision:

Schema policyHeader value
No policy resolved at allNo header written, in any mode
Private, or any named scopeCache-Control: private
Public, with a vary rule that is not scope_onlyCache-Control: private
Public, with a known maxAgeCache-Control: max-age=<n>, public
Public, with a maxAge and an swrCache-Control: max-age=<n>, stale-while-revalidate=<s>, public
Public, with no known maxAgeCache-Control: public

A stale window is written only beside a max-age, because that is what it qualifies: it says how long past the fresh window a stale answer stays acceptable, and with no window to be past it describes nothing. A stated swr: 0 is left off for the same reason, so the token that appears always means a window somebody asked for.

When the schema policy is private or names a named scope, GraphPilot writes Cache-Control: private in both non-off modes, with no freshness of any kind on it, so that a downstream CDN or shared cache does not store a response that is meant for one user or one group only. A private policy’s own swr still governs GraphPilot’s cache; it is simply not announced downstream. See who shares an entry for how a response ends up private in the first place.

The same holds for a public policy while any [[cache.policy.vary]] entry that is not scope_only is configured. Such an entry splits the edge’s entry by a value GraphPilot derives itself, so no Vary header could tell a downstream cache about the split, and the response is announced as private. An entry marked scope_only partitions only named scopes and leaves a public policy public.