Skip to content

Cache statuses

Every response that went through the cache carries a gp-cache header with one of four values: HIT, MISS, PASS or STALE. It tells you what the cache did with the request without opening a log, so it is the first thing to check when you want to know whether your caching rules apply.

A few things about the header itself:

  • GraphPilot removes the x-cache and x-cache-hits headers Fastly (the edge network GraphPilot runs on) adds, so gp-cache is the only cache status on a response.
  • It can be switched off with response.cache_headers = false. Most troubleshooting reads it, so a common pattern is to leave it on in a staging service and switch it off in production.
  • A request refused before the cache (a 401 from authentication, for example) carries no gp-cache header.
  • A service that runs in lean mode sends no gp-cache header on responses the cache would otherwise have looked up, because edge caching is withdrawn. A request that bypasses the cache still reports PASS with its reason.

All response headers GraphPilot sets are listed in the headers reference.

The response came from the cache. A stored entry matched the request and was still fresh, so your origin was not asked. A HIT carries no gp-cache-reason.

No usable entry existed, so the request went to your origin, and the response was stored. The next matching request at the same edge location can be a HIT. A MISS happens the first time an operation runs at a location, after an entry expires, and after a purge.

A MISS never carries gp-cache-reason. If the response was not stored, for whatever reason, the status is PASS.

A PASS means nothing was stored. The response came from your origin and was delivered to the client in full, but no cache entry answered the request and none was written. Every PASS carries gp-cache-reason, and the prefix of that code tells you where it was decided.

The cache was never consulted (CACHE_PASS_*). No lookup happened. This is decided before anything about the request is parsed, so it says nothing about the response: a bypass expression matched, the request was a WebSocket upgrade or used a method you never cache, a client profile takes the caller off the cache, and a few more. Every code is on the cache was not consulted.

The response was not stored (CACHE_SKIPPED_*). Usually the cache was looked up and missed, the request went to your origin, and then the request or the response ruled storing out: a mutation, a Set-Cookie, a no-store, no freshness lifetime, a GraphQL error. Two codes do not fit that pattern: CACHE_SKIPPED_ENTRY_NOT_REPLACED, where the lookup found an entry this request did not use, and CACHE_SKIPPED_NO_ORIGIN_RESPONSE, where no response came back from your origin at all. A few of these codes are faults on GraphPilot’s side rather than properties of your response, and they are still a PASS, because nothing was stored. Every code is on the response was not cached, and the order they are decided in is on when a response is cached.

A 4xx or 5xx response from your origin is also a PASS, with CACHE_SKIPPED_ERROR_STATUS, unless its status is listed in cache.policy.store_error_statuses. A GraphQL response that is not JSON GraphPilot can parse is delivered unchanged and not stored, with CACHE_SKIPPED_UNPARSEABLE_GRAPHQL_RESPONSE.

In Insights, both kinds of code are one column and one filter, Not cached because, grouped by who can act on the reason. The request’s Cache tab still says whether the cache was consulted before the decision.

The response came from the cache although the entry had passed its freshness lifetime, because it was still inside a stale window. The client got the stored answer immediately, and GraphPilot asked your origin for a fresh copy in the background. That background response goes through the normal store decision.

How long the window is, where it comes from and what switches it off is on stale windows. A MISS inside the window is possible too, when the request is handled by a cache server that does not hold the entry; see stale responses within a location.

gp-cache tells you what happened. On a PASS, a second header, gp-cache-reason, tells you why:

gp-cache: PASS
gp-cache-reason: CACHE_SKIPPED_SET_COOKIE

gp-cache-reason appears only on a PASS:

  • with a CACHE_PASS_* code, the cache was never consulted.
  • with a CACHE_SKIPPED_* code, the request reached your origin, the answer came back, and it was not stored.

A HIT, MISS or STALE carries no reason, because there is nothing to explain. A MISS is itself the signal that the entry was written and the next matching request at that location should be a HIT.

Every CACHE_SKIPPED_* code arrives with gp-cache: PASS, never with MISS. Every code, with what to do about it, is in the codes reference.

The value of gp-cache-reason is a stable code, not prose. The same code appears in your logs on the same request, so you can start from either end: notice the header in a browser’s network tab and search your logs for the code, or the reverse. gp-cache itself stays one of HIT, MISS, PASS or STALE and never carries the reason, so anything matching on it is unaffected.