Surrogate-Key vs Cache-Tag Across CDN Vendors
A team running the same application behind more than one CDN — or migrating between them — quickly discovers that “add a cache tag” is not one implementation but three incompatible ones. The header name, delimiter, cardinality ceiling, and even the plan tier required all differ between Fastly, Cloudflare, and Varnish. Shipping the wrong delimiter to the wrong vendor doesn’t error — it silently merges tags together and breaks purge granularity without any visible failure.
Prerequisite Concepts
- Surrogate Keys and Cache Tags — the general mechanism, tag design patterns, and cardinality rules this page assumes as background.
- Tag-Based Cache Invalidation Patterns — how a purge call propagates from the CDN control plane to edge PoPs once a tag has been emitted.
- Public vs Private Cache Scope —
publicstorage is a prerequisite on every vendor below; none of them will index a tag on a response they never stored.
Comparison Table
| Fastly | Cloudflare | Varnish (xkey/ykey) | |
|---|---|---|---|
| Header name | Surrogate-Key |
Cache-Tag |
xkey (request header consumed by the vmod; not stored as a response header) |
| Delimiter | Space-separated | Comma-separated | Space-separated |
| Plan requirement | All plans with VCL | Enterprise only | Self-hosted — no plan tier |
| Cardinality limit | 1,024 keys per object | 1,000 tags per object | No hard vendor limit; bounded by your VCL/header config |
| Header size limit | Governed by overall HTTP header size limits | 16KB per Cache-Tag header |
Governed by your own Varnish configuration |
| Purge API | POST /service/{id}/purge/{key} or bulk POST /service/{id}/purge with Surrogate-Key header |
POST /zones/{id}/purge_cache with {"tags": [...]} |
xkey.purge() inside VCL, or the XKEY-PURGE HTTP method against Varnish directly |
| Soft purge support | Yes — Fastly-Soft-Purge: 1 |
No native soft purge on tag purges | Yes — xkey.softpurge() |
Step-by-Step Resolution
Step 1 — Pick one vendor-agnostic tag naming convention
Decide on a stable tag string before touching any vendor-specific header — for example product-9472 for a single entity and category-footwear for its grouping. This string is what you’ll relay into each vendor’s header format below; keeping it identical across vendors makes multi-CDN or migration setups predictable.
product-9472
category-footwear
Step 2 — Emit the tag on Fastly
Fastly reads Surrogate-Key directly from the origin response, space-delimited:
Cache-Control: public, s-maxage=86400
Surrogate-Key: product-9472 category-footwear
Strip it before delivery in VCL so it never reaches the client:
sub vcl_deliver {
unset resp.http.Surrogate-Key;
}
Step 3 — Emit the tag on Cloudflare
Cloudflare reads Cache-Tag, comma-delimited, and only honors it on an Enterprise plan:
Cache-Control: public, s-maxage=86400
Cache-Tag: product-9472,category-footwear
Strip it via a Transform Rule (Rules → Transform Rules → Modify Response Header → Remove Cache-Tag) since Cloudflare does not strip vendor tag headers automatically before they reach the browser.
Step 4 — Emit the tag on Varnish (xkey)
Varnish’s xkey vmod attaches keys to the object during vcl_backend_response, reading them from an origin-supplied header rather than storing the header itself on the client-facing response:
import xkey;
sub vcl_backend_response {
if (beresp.http.X-Cache-Tags) {
set beresp.http.xkey = beresp.http.X-Cache-Tags;
}
}
sub vcl_deliver {
unset resp.http.X-Cache-Tags;
}
Origin sets a plain internal header (X-Cache-Tags: product-9472 category-footwear), and the vmod does the indexing — no vendor-shaped header needs to leave your application layer at all.
Step 5 — Purge by tag on each vendor
Fastly:
curl -X POST "https://api.fastly.com/service/$SERVICE_ID/purge/product-9472" \
-H "Fastly-Key: $FASTLY_TOKEN"
Cloudflare:
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tags": ["product-9472"]}'
Varnish (xkey, direct HTTP purge method):
curl -X XKEY-PURGE -H "xkey-purge: product-9472" https://varnish.internal/
Step 6 — Confirm the purge fired on all affected URLs
Regardless of vendor, re-request every URL known to share the tag and confirm a fresh fetch:
curl -sI https://yourdomain.com/products/9472 | grep -iE 'age|x-cache|cf-cache-status'
curl -sI https://yourdomain.com/category/footwear | grep -iE 'age|x-cache|cf-cache-status'
Expected Output / Verification
A successful emit-and-purge cycle looks like this on any vendor:
- Before purge: both URLs return
Age > 0and a cache-hit status (HIT,CF-Cache-Status: HIT, or Varnish’sX-Cache: HIT). - Immediately after purge: both URLs return
Age: 0(or the header is absent entirely on the first post-purge response) and a cache-miss status. - A few seconds later, re-requesting shows
HITagain withAgeincrementing from0— confirming the cache repopulated from a fresh origin fetch rather than continuing to serve a stale copy. - The tag header itself (
Surrogate-Key,Cache-Tag) must never appear in the client-facing response on any vendor. If it does, the egress-stripping step for that vendor was skipped or misconfigured. - On Cloudflare, the purge API response body includes
"success": true; afalsevalue with an error array usually means the zone is not on an Enterprise plan or the tag string exceeded the 16KB header ceiling upstream.
Edge Cases
- Delimiter confusion when migrating vendors — a team moving from Fastly to Cloudflare that copies
Surrogate-Key: product-9472 category-footwearverbatim intoCache-Tagcreates a single tag containing a literal space, not two tags. Always translate the delimiter, not just the header name. - Cloudflare Enterprise gating —
Cache-Tagpurges silently fail (or the header is ignored) on non-Enterprise Cloudflare plans. There is no lower-tier partial support; teams on Pro or Business plans must purge by URL or hostname instead. - Varnish has no purge API to call remotely by default — unlike Fastly and Cloudflare, which expose a purge endpoint over the public internet, Varnish’s
xkey.purge()runs inside VCL or via a direct HTTP method against the Varnish instance itself. Exposing that safely usually requires an internal-only purge endpoint or an authenticated proxy in front of Varnish. - CloudFront has no equivalent tagging primitive — AWS CloudFront invalidations operate on path patterns, not metadata. Multi-CDN setups that include CloudFront typically front it with a tagging-capable layer or fall back to path-based invalidation for that vendor specifically.
- Mixed-case or reused tag strings across environments — a staging and production environment sharing the same origin code but different CDN accounts can accidentally purge the wrong environment’s cache if tag strings aren’t environment-scoped (e.g.
product-9472vsproduct-9472-staging).
Frequently Asked Questions
Does Fastly require an Enterprise plan for Surrogate-Key?
No. Fastly includes Surrogate-Key purging on every service plan that supports VCL. Cloudflare is the outlier here — Cache-Tag purging is Enterprise-only.
Can I send the same tag string to both Fastly and Cloudflare?
The tag value itself can be identical, but the header and delimiter must be translated: Surrogate-Key: product-9472 category-footwear for Fastly versus Cache-Tag: product-9472,category-footwear for Cloudflare. Sending the wrong delimiter to a vendor merges what should be separate tags into one.
Which vendor has the highest tag cardinality limit?
Fastly allows up to 1,024 Surrogate-Key values per object. Cloudflare Enterprise allows up to 1,000 Cache-Tag values with an additional 16KB header size ceiling. Varnish’s xkey module has no hard-coded vendor limit — practical ceilings come from your own VCL and header-size configuration.
Does AWS CloudFront support the same kind of tag purging?
Not natively. CloudFront invalidations target URL path patterns rather than arbitrary metadata tags. To approximate tag-based purging in a CloudFront deployment, pair it with a custom cache policy and invalidate by path pattern, or place a tagging-capable layer such as Fastly or Varnish in front of it.
Related
- Purging Cloudflare Cache Tags with the API — a deeper walkthrough of the Cloudflare-specific purge endpoint, authentication, and response handling.
- Fastly Surrogate-Key Invalidation Patterns — Fastly-specific soft purge and bulk purge techniques beyond the basics shown here.
- How CDN Cache Keys Are Generated — the underlying per-vendor key construction that every tag index sits on top of.
Back to Surrogate Keys and Cache Tags