Edge caching, explained: from cache keys to stale-if-error
Every site on Benediction sits behind a cache. Most of the time that cache is invisible — requests answered in single-digit milliseconds, origins untroubled. But caching is a system with a surprising number of moving parts. This post walks through how the pieces fit together, and where tuning actually moves the needle.
The three questions every cache answers
A cache is just a function of three questions:
- Should I store this? — Cacheability, driven by status code and response headers.
- Under what key? — Cache keys, derived from URL and request attributes.
- When should I let it go? — Expiration, plus active invalidation.
Get the first one wrong and you serve stale or private data. Get the second wrong and cache hit ratios collapse. Get the third wrong and you ship updates nobody sees. Let's take each in turn.
Cacheability: what gets stored
By default we honor standard HTTP semantics. A response is cacheable when it has an explicit freshness signal:
Cache-Control: public, max-age=3600 → cacheable, 1 hour Cache-Control: no-store → never cached Cache-Control: private, max-age=3600 → cached but never shared across users Set-Cookie: session=… → uncacheable unless you say otherwise
The one rule that surprises people: Set-Cookie alone does not disable caching in HTTP semantics — but we treat it as a strong signal and default to bypass. If you need cookie-aware responses cached, use cache keys instead of disabling caching entirely.
Cache keys: what distinguishes two copies
Two requests can hit the same URL and deserve different responses — different devices, different cookies, different languages. Cache keys let you encode those differences:
gitflare cache keys add example.com \ --include device.type language.header.accept-language # now cache stores separate copies per device class + language, # so /products never mixes desktop-HTML with mobile-HTML.
The tradeoff is real: every attribute you add to the key fragments your cache. Add too many and you're back to origin hits. Our guidance is to include only attributes that actually change the response body.
Expiration and invalidation
Expiration is the lazy path — wait for max-age to pass, then revalidate. Invalidation is the eager path:
# one file gitflare cache purge example.com --url https://example.com/assets/app.js # everything that changed in a release (cache tags) gitflare cache purge example.com --tag release-2026-07 # the whole zone gitflare cache purge example.com --everything
Cache tags are the sweet spot for most teams: tag responses at the origin, then purge a tag instead of enumerating URLs. A single purge command after a deploy replaces hundreds of individual URL purges.
Surviving origin failures: stale-if-error
When an origin is slow or down, the worst thing a cache can do is add its own failure on top. With stale-if-error enabled, we serve the previous good response while revalidating in the background:
gitflare cache stale example.com \ --on error serve-stale \ --revalidate every 30s # origin down? visitors still get the last good copy, # refreshed in the background the moment the origin recovers.
In our traffic, zones that enable this see origin-outage impact drop from "site down" to "occasionally 30s-stale pages." It's the single highest-leverage resilience setting we offer.
A worked example
Here's a realistic recipe that lifted a customer's cache hit ratio from 62% to 94% in an afternoon:
1. Cache-Control: public, max-age=86400 on all static assets 2. Cache key: device.type for the HTML routes 3. Cache tags on release builds → purge by tag after deploy 4. stale-if-error enabled on HTML + API GETs 5. Long TTL on hashed assets (app.a1b2c3.js never needs revalidation)
When to think twice
Caching is not free. Personalized feeds, per-user quotas, and anything with real-time semantics should probably bypass or fragment heavily. A good rule of thumb: if the response differs per-user or changes faster than you can purge, it doesn't belong in a shared cache.
Want to try these on your own zone? The CLI reference covers every flag, and our docs walk through advanced cache-key expressions.