Fastly Surrogate-Key Invalidation Patterns
An e-commerce catalog served through Fastly typically renders one product across a product detail page, a category listing, several search result pages, and a handful of merchandising widgets. A price change or stock update means all of those cached objects need to be evicted together — but a hard eviction across a busy catalog can send a synchronized burst of requests back to the origin. Fastly’s Surrogate-Key mechanism, combined with the choice between soft and hard purge, lets you invalidate by logical entity while controlling exactly how disruptive that invalidation is to the origin.
Prerequisite Concepts
This pattern builds directly on three ideas covered elsewhere on this site:
- Surrogate keys and cache tags — the vendor-neutral concept of tagging cached responses so they can be purged as a group.
- Tag-based cache invalidation patterns — how the edge builds a tag-to-cache-key index and where purge dispatch sits relative to RFC 9111 freshness.
- Serving stale content with
stale-while-revalidate— the grace-period model that Fastly’s soft purge borrows from conceptually, even though it is triggered explicitly rather than by TTL expiry.
Step-by-Step Resolution
Step 1 — Emit Surrogate-Key from the origin
Fastly reads keys from a space-delimited Surrogate-Key response header — the opposite convention from Cloudflare’s comma-delimited Cache-Tag, covered in purging Cloudflare cache tags with the API:
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: public, s-maxage=86400, stale-while-revalidate=3600
Surrogate-Key: product-9472 category-footwear brand-acme
The response needs an explicit shared-cache TTL — s-maxage — for Fastly to store it at all. A response marked private or no-store is never cached at the edge, so any Surrogate-Key on it is inert.
Step 2 — Confirm the key survives vcl_deliver
Custom VCL runs between the origin response and the client, and it’s common to accidentally strip or overwrite Surrogate-Key in vcl_fetch before Fastly’s cache layer indexes it, or forget to strip it in vcl_deliver before it reaches the browser. A minimal, correct pair of subroutines:
sub vcl_fetch {
#FASTLY fetch
# Origin already emits Surrogate-Key; optionally append a broad
# page-type key here for coarse-grained invalidation.
if (beresp.http.Surrogate-Key) {
set beresp.http.Surrogate-Key = beresp.http.Surrogate-Key " page-type-product";
}
}
sub vcl_deliver {
#FASTLY deliver
# Surrogate-Key has no meaning to the browser — never leak it.
unset resp.http.Surrogate-Key;
}
The #FASTLY fetch and #FASTLY deliver comments preserve Fastly’s own boilerplate logic when you add custom VCL — omitting them silently breaks default caching behavior. Confirm the key reached the cache layer (not just the origin response) by checking Fastly’s debug header:
curl -sI -H "Fastly-Debug: 1" https://shop.example.com/products/9472 \
| grep -iE 'surrogate-key|x-cache|fastly-debug'
Step 3 — Hard-purge a single key
Fastly’s single-key purge endpoint is POST /service/{service_id}/purge/{surrogate_key}. With no additional headers, this performs a hard purge: the object is deleted outright, and the next request is a synchronous miss to the origin.
curl -X POST "https://api.fastly.com/service/SU1Z0isxPaozGVKXdv0eY/purge/product-9472" \
-H "Fastly-Key: YOUR_API_TOKEN" \
-H "Accept: application/json"
{"status": "ok", "id": "7SO8DqiHNCFkPnldZbTKlY-SJTNJHTTP-DFW"}
Every object tagged product-9472 — the PDP, the cart fragment, the search result snippet — is evicted from every PoP in one call.
Step 4 — Soft-purge multiple keys in a single bulk call
For a catalog-wide update touching many entities at once, use the bulk purge endpoint with a Surrogate-Key header listing every key, and add Fastly-Soft-Purge: 1 to avoid a synchronized origin stampede:
curl -X POST "https://api.fastly.com/service/SU1Z0isxPaozGVKXdv0eY/purge" \
-H "Fastly-Key: YOUR_API_TOKEN" \
-H "Fastly-Soft-Purge: 1" \
-H "Surrogate-Key: product-9472 category-footwear brand-acme"
With Fastly-Soft-Purge: 1, Fastly marks each matching object stale instead of deleting it. If stale-while-revalidate is present on the original response, the next request is served the stale copy immediately while Fastly revalidates against the origin in the background — the object never disappears from cache, it’s just no longer considered fresh. This is the preferred default for high-traffic catalog pages, where a hard purge on a hot key could otherwise send hundreds of simultaneous requests to origin.
Practical size limit: the Surrogate-Key request header in a bulk call is still an HTTP header, subject to the same ~8 KB header-line limits most proxies enforce. Fastly documents a ceiling of 1,024 keys per object, but in a bulk purge request the header-size limit typically binds first — batch large invalidation jobs (hundreds of entities) into several smaller calls rather than one very long header value.
Step 5 — Verify with X-Cache and confirm the correct purge behavior fired
After a hard purge, the next request should show an immediate miss:
curl -sI https://shop.example.com/products/9472 | grep -iE 'x-cache|age'
X-Cache: MISS
Age: 0
After a soft purge, the next request should show a stale hit (Fastly still serves it) with the object subsequently revalidating in the background — a repeat request moments later should show Age: 0 on the freshly revalidated copy:
curl -sI https://shop.example.com/products/9472 | grep -iE 'x-cache|age'
# X-Cache: HIT (stale copy served under stale-while-revalidate)
sleep 2
curl -sI https://shop.example.com/products/9472 | grep -iE 'x-cache|age'
# X-Cache: MISS, Age: 0 (background revalidation completed)
Expected Output / Verification
purgeandpurge/{key}calls return{"status": "ok", "id": "..."}— anything else indicates the token lacks purge scope for the service, or the service ID is wrong.- After a hard purge,
X-CachereadsMISSwithAge: 0on the very next request for every URL sharing the key. - After a soft purge,
X-Cachemay still readHITon the immediate next request (the stale copy, served understale-while-revalidate), transitioning to a freshMISS/Age: 0pair once background revalidation completes. Fastly-Debug: 1on a warm request shows theSurrogate-Keyvalues Fastly indexed for that object, letting you confirm a key was actually attached before assuming a purge should have matched it.
Edge Cases
- Space vs. comma delimiters — Fastly requires space-separated keys; sending a Cloudflare-style comma-separated list produces a single malformed key rather than several distinct ones. If your platform serves both CDNs, generate the header per vendor — see purging Cloudflare cache tags with the API for the comma-delimited equivalent, and surrogate-key vs cache-tag across CDN vendors for a full comparison.
- Key cardinality explosion — assigning a unique key per user session or per request parameter, rather than per logical entity, produces enormous index tables and slows purge lookups. Design keys around stable entities: product ID, category slug, brand, warehouse region, and page type (
page-type-pdp,page-type-category), not per-request values. - Soft purge without
stale-while-revalidate— if the original response never setstale-while-revalidate, a soft purge still marks the object stale, but Fastly has no grace window to serve it from; behavior converges on a hard purge for practical purposes. Always pair soft-purge workflows with an explicit stale-serving window. - VCL evaluation order — if other custom VCL logic in
vcl_fetchruns after the block shown above and overwritesberesp.http.Surrogate-Keywholesale rather than appending to it, previously set keys are silently lost. Audit the fullvcl_fetchchain, not just the snippet you added.
Frequently Asked Questions
What is the difference between soft purge and hard purge on Fastly?
A hard purge immediately deletes the object from cache; the next request is a synchronous miss that goes straight to the origin. A soft purge marks the object stale but keeps it available for grace-period delivery under stale-while-revalidate while Fastly revalidates in the background, avoiding a synchronized origin traffic spike.
Is surrogate-key purging available on all Fastly plans?
Yes. Unlike Cloudflare’s Enterprise-only Cache-Tag, Fastly includes Surrogate-Key purging — both single-key and bulk — on every service plan that supports VCL, including the free developer tier.
Can I purge keys from within VCL without calling the API?
Yes, using ban or restart logic inside custom VCL for request-time conditions, but most teams prefer the HTTP purge API for deploy-pipeline integration and audit logging. Reserve VCL-side purging for edge logic that needs to react to something in the request itself.
How many surrogate keys can I attach to one response?
Fastly documents a practical ceiling of 1,024 surrogate keys per object. In practice, the ~8 KB HTTP header size limit on the Surrogate-Key value itself usually becomes the binding constraint before you approach that documented count.
Related
- How CDN cache keys are generated — the primary cache key structure that a surrogate-key index sits alongside at the edge.
- Mapping Vary headers to edge routing — how
Varydimensions multiply the number of cached variants a single key must cover. - Origin shielding and request collapsing — verify that purge signals propagate through the shield tier, not just the outer edge PoPs.
- Interpreting X-Cache and CF-Cache-Status headers — the diagnostic reference for reading the cache-status headers used throughout this verification workflow.
Back to Surrogate Keys and Cache Tags