Stale windows
A stale window is a number of seconds after an entry’s lifetime during which GraphPilot still
answers from the cache, while a fresh copy is fetched behind it. Inside the window the caller
gets the stored answer immediately, with no origin round trip, and the response reports
gp-cache: STALE rather than HIT. The refresh runs after the answer has left, and the next
caller gets the new copy. Inside an edge location, the stale copy is served by the cache server that
holds the entry, so a request that another server handles can occasionally be a MISS (see
stale responses within a location).
That makes it the cheap half of a hit rate. The lifetime decides how fresh the served copy is guaranteed to be; the stale window decides how long you keep answering instantly after that guarantee runs out. Widening it costs nothing in latency and buys back every request that used to wait for your origin at the moment an entry expired.
Where the window comes from
Section titled “Where the window comes from”From the same source that decided the lifetime (see precedence):
-
Your origin’s header, as
stale-while-revalidate:Cache-Control: max-age=60, stale-while-revalidate=86400 -
Your schema, as the
swrargument of@cacheControl(GraphQL only):schema.graphql type Query {# Guaranteed fresh for a minute. For the day after that, still answered# from the cache while a refresh runs behind the response.invoices: [Invoice!]! @cacheControl(maxAge: 60, swr: 86400)}
A response with neither goes straight from fresh to a MISS.
How the schema resolves a window across a query
Section titled “How the schema resolves a window across a query”A field that states no window does not switch one off. The response’s window is the minimum
over the fields that declared one, and a field that declares none contributes nothing. This is
deliberately not the rule maxAge follows, where an unannotated field counts as zero and takes
the whole query with it.
An unannotated field is a field nobody has said is safe to hold, so maxAge treats silence as a
refusal. Staleness is not that kind of statement: it moves no audience, the discriminators an
entry is keyed on are untouched, and a purge ends staleness the moment something writes. If
silence meant zero here, one unannotated wrapper type anywhere in a selection would switch the
window off for the whole query, with no symptom but a hit rate that never moves.
A stated swr: 0 is still a statement, and it wins the minimum like any other number. That is how
you switch staleness off for a response on purpose.
What switches a window off
Section titled “What switches a window off”must-revalidateorproxy-revalidateon the origin header. Both forbid serving a response once it is stale, and GraphPilot has no conditional revalidation. Somax-age=60, stale-while-revalidate=600, must-revalidateis stored for 60 seconds and never servedSTALE. Your logs recordCACHE_STORED_WITHOUT_STALE_WINDOWwith the window that was dropped.- A hard purge. A purge removes the entry outright rather than aging it. Only a soft purge leaves it to be served stale.
stale-if-error is not a stale window in this sense: GraphPilot parses it and does not act on it,
so an entry is not served stale because your origin failed. See from the origin
header.
s-maxage=0 with a stale window
Section titled “s-maxage=0 with a stale window”s-maxage=0 keeps a response from ever being served fresh from the edge, and it combines with
stale-while-revalidate in a way that is specified rather than a quirk.
RFC 5861 lets a cache serve a response “after
it becomes stale, up to the indicated number of seconds”, with no exemption for why it became
stale. So s-maxage=0, stale-while-revalidate=60 reads as “stale to a shared cache immediately,
and you may serve it stale for a minute”, and GraphPilot does exactly that: STALE for the
window.
If what you mean is that the edge should keep nothing, Cache-Control: no-store says so with no
interactions at all.
Stale windows and request collapsing
Section titled “Stale windows and request collapsing”When a popular entry expires without a window, the next requests for it wait on one origin request: request collapsing sends one and holds the rest. With a window, a request that reaches the entry inside the window is served it immediately, and the refresh runs behind it. A request that does not reach it goes to your origin, as described in stale responses within a location.
The background response goes through the whole store
decision like any other. If it is
not storable, for example because your origin now answers with Set-Cookie, the stale entry is
not replaced, and the refusal code appears in your logs rather than on a response.
Stale responses within a location
Section titled “Stale responses within a location”An edge location (a Fastly POP) is not one cache but several cache servers. Each cached entry
has one server in the location that holds it. While the entry is fresh, the location’s other
servers can answer from it too, as a HIT. Once its lifetime has run out, the stale copy is
served by the server that holds the entry.
In practice, most requests inside the window are STALE. Occasionally a request that another
server handles finds no entry: it is a MISS, goes to your origin, and stores a fresh copy that
the following requests are served from. This is expected behavior, not an error. It never serves
wrong or older data; it costs one origin request per such miss.
An example, for an entry stored with maxAge: 60, swr: 3600, with all four requests at the same
location:
| Time | Server | gp-cache | gp-cache-age | Insights Lookup | What happened |
|---|---|---|---|---|---|
| 0 s | A | MISS | No entry | Your origin answered; the entry was stored. | |
| 0.2 s | B | HIT | 0 | Entry found | Fresh, so server B could answer from it. |
| 93 s | A | STALE | 93 | Entry found | Past its lifetime, inside the window. Refreshed in the background. |
| 93 s, 16 ms later | B | MISS | No entry | Server B did not find the stale entry. Your origin answered; a fresh copy was stored. |
To recognize it, compare the requests in Insights: the request detail
panel’s Cache tab shows the status and the Lookup (Entry found or No entry), and
its Overview tab shows the edge location and the Server within it. A MISS with No
entry at the same location and within a few seconds of a STALE, on a different Server,
is this case. The detail panel shows these fields on every plan.
With shielding
Section titled “With shielding”With shielding (paid plans, not Glider (Free)), such a miss is sent to the shield location instead of your origin. If the shield still holds the entry, fresh or inside its stale window, it answers and your origin is not contacted. The edge location still reports what it saw itself:
- The shield’s copy was fresh:
gp-cache: MISS, Shield resultHIT. The edge location stores the copy for the lifetime it has left. - The shield’s copy was stale:
gp-cache: PASSwithgp-cache-reason: CACHE_SKIPPED_SHIELD_RESPONSE_NOT_STORABLE, Shield resultSTALE. The edge location does not store a stale copy; the shield refreshes its own in the background.
Shielding does not change how a location’s servers share entries, and the shield is itself a
location with several servers: a request that reaches a shield server without the entry after
its lifetime is a miss there and goes on to your origin. Shielding reduces how often your origin
is asked; it does not guarantee a STALE at the edge.
If origin load in the window matters
Section titled “If origin load in the window matters”- Switch on shielding, so most of these misses are answered by the shield instead of your origin.
- Use a longer lifetime (
maxAgeors-maxage). While an entry is fresh, every server in the location answers from it, so fewer requests fall into the window at all. With invalidation on write, a long lifetime does not mean old data.
Choosing a window
Section titled “Choosing a window”A window also extends how long an entry keyed to one caller may be served after that caller’s access changed: the bound is the lifetime plus the window. See revocation.
A private or named-scope response keeps its window in GraphPilot’s cache, but the window is not announced to the client. See downstream header.