Tagging Strategies for E-commerce Catalog Pages
Problem Statement
One product’s price changes. That product appears on its own detail page, on four category listings, in three merchandising carousels, in a sitemap fragment and in a search results page. Purging by URL requires the publishing system to know all of them; purging by tag requires only that each page declared what it rendered. The design question is which tags to emit, on which pages, without exceeding the vendor’s header budget.
Prerequisite Concepts
- Tag-based cache invalidation patterns — the reverse index that makes a tag purge expand into a URL set.
- Surrogate keys and cache tags — how tags are emitted and how the edge stores them.
- Cache key normalization and query strings — why tags cover variants that a URL purge would miss.
Step-by-Step Resolution
Step 1 — Map the fan-out
Before choosing tags, write down every page type on which one product’s data appears, and how many URLs each represents:
The unbounded row is the one that decides the design: search results cannot be enumerated, so anything that relies on listing URLs will always miss some.
Step 2 — Assign tags per entity and per grouping
The detail page declares the entity it renders and the groupings it belongs to:
GET /products/9472
Surrogate-Key: product-9472 collection-boots brand-alden layout-pdp
Cache-Control: public, s-maxage=86400
An aggregate page declares the grouping rather than every entity:
GET /categories/boots?page=3
Surrogate-Key: collection-boots layout-plp
Cache-Control: public, s-maxage=3600
Step 3 — Cap the tag count on aggregates
The temptation is to tag a category page with every product it renders, so that any product change refreshes the listing. On a forty-product page with pagination that is forty tags per URL, and vendors impose a header budget:
# Emit entity tags only where the page is about that entity
tags = [f"collection-{collection.slug}", f"layout-{template}"]
if page_type == "detail":
tags.insert(0, f"product-{product.id}")
tags += [f"brand-{product.brand.slug}"]
# Aggregates never carry per-product tags
response["Surrogate-Key"] = " ".join(tags[:8])
The trade is explicit: a price change refreshes the whole category listing rather than only the row that changed. Since a listing is a single response, there is no finer unit to invalidate anyway.
Step 4 — Wire the purge to the domain event
Each kind of change purges a different tag, and getting this mapping right is what keeps the blast radius proportional:
Step 5 — Verify the blast radius in both directions
A purge test must confirm both that affected URLs were evicted and that unaffected ones were not:
# Purge the entity tag
curl -sX POST "https://api.fastly.com/service/$SERVICE_ID/purge/product-9472" \
-H "Fastly-Key: $FASTLY_API_TOKEN"
# Affected: expect a MISS
curl -sI https://example.com/products/9472 | grep -iE 'x-cache|^age'
# Unaffected: expect a HIT with a climbing Age
curl -sI https://example.com/products/1001 | grep -iE 'x-cache|^age'
A miss on the second URL means the tag is too coarse and every product change is repopulating the catalogue.
Expected Output / Verification
A correct scheme produces a tight, predictable relationship between an edit and the URLs that go cold. After a price change, the product’s own variants miss, the category listing misses if it displays price, and everything else continues to hit:
/products/9472 MISS Age: 0 <- expected
/categories/boots?page=1 MISS Age: 0 <- expected, shows price
/products/1001 HIT Age: 41202 <- correctly untouched
/guides/sizing HIT Age: 86011 <- correctly untouched
It is worth capturing this table for each event type once and keeping it with the tagging code, because the failure mode of a tagging scheme is silent: a tag that never matches anything purges nothing and reports success.
Edge Cases
- Header budget truncation. Vendors differ in whether an over-long tag header errors or truncates. Truncation is the dangerous case, because the dropped tags fail silently at purge time. Cap the count in the application.
- Variants inheriting tags correctly. Every
Varyvariant of a response carries the same tags, which is precisely why tag purging is more reliable than URL purging on a site with encoding or locale variants. - Search results that cannot be tagged. A results page assembled from a search index may not know which entities it rendered. Give it a short lifetime instead of relying on invalidation.
- Tags on error responses. A
404for a deleted product should carry the product tag, so that restoring the product purges the negative response along with everything else. - Renaming a slug. A tag derived from a slug changes when the slug changes, orphaning the old tag and leaving pages that still reference it uninvalidatable. Derive tags from immutable identifiers, not from human-readable names.
- Purge quota under bulk imports. A catalogue import that edits ten thousand products issues ten thousand purges. Batch by collection tag rather than purging per entity.
Frequently Asked Questions
Should a category page carry a tag for every product on it?
Usually not. A page rendering forty products would need forty tags, and aggregate pages multiply that across pagination. Tag the collection instead and accept that a single product change refreshes the listing along with the rest of the category.
What happens when the tag header exceeds the vendor limit?
Behaviour varies: some vendors truncate silently, which means the tags after the cut-off never purge anything. That is the worst failure mode in tagging because everything appears to work until a purge quietly misses. Cap the count in the application rather than relying on the edge.
Is one tag per product enough?
Not for a real catalogue. You also need tags for the collections a product belongs to, for the template that renders it, and usually for the price or availability feed, because each of those changes affects a different set of URLs.
How granular should the tags be?
Granular enough that a purge does not repopulate more than the origin can absorb, and coarse enough that the tag set fits the header budget. In practice that means entity tags on detail pages and collection tags on aggregates.
Related
- Purging Cloudflare cache tags with the API covers the purge call this scheme depends on.
- Surrogate-Key vs Cache-Tag across CDN vendors explains the header syntax and budget differences between vendors.
- Event-driven cache invalidation with webhooks is the delivery mechanism that turns a catalogue edit into a purge.