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:

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)
Choosing Hard Purge vs Soft Purge Decision flowchart: if the origin cannot absorb a synchronous miss burst, or brief staleness is acceptable during revalidation, use a soft purge; otherwise use a hard purge, optionally pre-warming the cache with a synthetic request afterward. Tag purge needed Can origin absorb a synchronous miss burst? Yes Hard purge (immediate evict) No Is brief staleness OK during revalidation? Yes Soft purge (Fastly-Soft-Purge: 1) No Hard purge, then pre-warm via synthetic request

Expected Output / Verification

  • purge and purge/{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-Cache reads MISS with Age: 0 on the very next request for every URL sharing the key.
  • After a soft purge, X-Cache may still read HIT on the immediate next request (the stale copy, served under stale-while-revalidate), transitioning to a fresh MISS/Age: 0 pair once background revalidation completes.
  • Fastly-Debug: 1 on a warm request shows the Surrogate-Key values 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 set stale-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_fetch runs after the block shown above and overwrites beresp.http.Surrogate-Key wholesale rather than appending to it, previously set keys are silently lost. Audit the full vcl_fetch chain, 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.


Back to Surrogate Keys and Cache Tags