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

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:

Where one product's data appearsA single product change affects several page types with very different cardinalities, which is what makes a URL-based purge unreliable.URLs affectedTag to carryProduct detail1 to 4 variantsproduct-9472Category listing8 to 40 paginatedcollection-bootsSearch resultsunboundedcollection-bootsMerchandising carousel3 to 12collection-bootsSitemap fragment1catalog-indexPrice feed1catalog-index
One product change, six page types, hundreds of URLs

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
One edit, one purge call, the full fan-outThe publisher names the entity that changed; the edge expands the tag into every stored URL that declared it, including variants and paginated listings.Price editproduct 9472webhookPurge calltag: product-9472APITag indexexpands to URLsfan-outEvicted4 detail variants
The publisher never needs to know the URLs

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:

Domain event to tag, and the resulting blast radiusThe tag purged should match what actually changed, because over-purging costs a repopulation spike and under-purging leaves stale pages.Purge tagURLs evictedOrigin impactPrice or stock changeproduct-94724 to 12negligibleProduct added or removedcollection-boots40 to 400moderateCategory re-sortedcollection-boots40 to 400moderateDetail template changedlayout-pdpall productshighBrand renamedbrand-aldenbrand pages plus detailsmoderate
Five events, five different purge scopes

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 Vary variant 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 404 for 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.


Back to Tag-Based Cache Invalidation Patterns