Invalidating Nested Content with Hierarchical Tags
Problem Statement
A documentation section is renamed. Every page beneath it shows the section name in its breadcrumb, its navigation and its title suffix, so all of them are now wrong — but only those, not the rest of the site. Tags are a flat namespace with no notion of containment, and the naive options are both bad: purge everything, or enumerate the descendants.
Prerequisite Concepts
- Tag-based cache invalidation patterns — the tag index and how a purge expands through it.
- Surrogate keys and cache tags — the header mechanics and vendor differences.
- Tagging strategies for e-commerce catalog pages — the flat scheme this technique builds on.
Step-by-Step Resolution
Step 1 — Model the hierarchy as path prefixes
Every node in a tree is identified by the chain of ancestors above it. That chain can be expressed as a set of flat tags, one per level:
/guides/http/caching/validators
-> guides
-> guides.http
-> guides.http.caching
-> guides.http.caching.validators
Each tag names a subtree. Purging guides.http.caching invalidates everything below it and nothing above or beside it.
Step 2 — Emit one tag per level
The chain is computed from the page’s own path, so it needs no external index:
def prefix_tags(path, max_depth=6):
parts = [p for p in path.strip("/").split("/") if p][:max_depth]
return [".".join(parts[:i + 1]) for i in range(len(parts))]
# /guides/http/caching/validators ->
# ['guides', 'guides.http', 'guides.http.caching', 'guides.http.caching.validators']
response["Surrogate-Key"] = " ".join(prefix_tags(request.path))
GET /guides/http/caching/validators
Surrogate-Key: guides guides.http guides.http.caching guides.http.caching.validators
Cache-Control: public, s-maxage=86400
Step 3 — Purge at the level that changed
The rule is to purge the narrowest tag that covers the change:
# A section rename invalidates the section and everything beneath it
curl -sX POST "https://api.fastly.com/service/$SERVICE_ID/purge/guides.http" \
-H "Fastly-Key: $FASTLY_API_TOKEN"
Step 4 — Bound the depth
A deep tree produces long tag headers, and vendors impose a budget. Capping the depth means very deep pages share their deepest ancestor tag rather than having their own — acceptable, because a page that deep is rarely purged individually:
tags = prefix_tags(request.path, max_depth=5)
Five levels covers almost any documentation tree, produces at most five short tags, and leaves room in the header for the entity and template tags a page also needs.
Step 5 — Verify the subtree boundary
The test that matters is that the boundary is where you think it is — descendants gone, siblings untouched:
curl -sX POST "https://api.fastly.com/service/$SERVICE_ID/purge/guides.http.caching" \
-H "Fastly-Key: $FASTLY_API_TOKEN"
for p in /guides/http/caching/validators \
/guides/http/caching/directives \
/guides/http/methods \
/guides/css/layout; do
printf '%-38s ' "$p"
curl -sI "https://example.com$p" | awk -F': ' '/[Aa]ge/{print "Age " $2}'
done
The first two should report Age near zero; the last two should report a climbing Age. Anything else means the prefix chain is not being emitted consistently.
Expected Output / Verification
A correct implementation gives a clean cut at the purged level:
/guides/http/caching/validators Age 1 <- evicted, correct
/guides/http/caching/directives Age 2 <- evicted, correct
/guides/http/methods Age 41204 <- sibling, untouched
/guides/css/layout Age 86022 <- unrelated, untouched
Repeat the check after any change to the routing layer, since a path rewrite that alters the segments a page sees will silently change the tags it emits and therefore which purges reach it.
Edge Cases
- A page reachable by several paths. Emit one prefix chain per path. The tag count grows but remains far smaller than the URL set a purge would otherwise need.
- Rewrites changing the visible path. If the application sees a different path from the one the visitor requested, the tags describe the internal tree rather than the public one, and purges by public path will miss.
- A depth cap that hides a real level. Capping at five levels on a tree that is genuinely seven deep means the two deepest levels cannot be purged independently. Confirm the cap against the real maximum depth.
- Trailing-slash variants.
/guides/http/and/guides/httpproduce the same segment list, so the tags match — but only if the splitting logic strips empty segments, which is worth a unit test. - Very large subtree purges. Purging a top-level tag can evict thousands of pages at once. Use a soft purge where the vendor supports it so the edge keeps answering while it repopulates.
- Sibling pages that quote a parent’s content. A page outside the subtree that renders a fragment of the renamed section is not covered by the prefix chain and will keep showing the old name. Where that pattern exists, the quoting page should also emit the quoted node’s tag, which is the point at which a pure hierarchy needs a flat entity tag alongside it.
- Localised trees. A locale segment at the root multiplies every tag by the number of locales. Either include the locale in the chain deliberately, or strip it so a content change purges every language at once.
Frequently Asked Questions
Why not just purge by URL prefix?
Most vendors do not support prefix purging at all, and those that do usually restrict it to higher plan tiers or impose rate limits. Path-prefix tags achieve the same effect using the ordinary tag mechanism, which is universally available.
How many levels should a page tag?
As many as the hierarchy has, up to the header budget. Four to six levels covers almost every real content tree, and the tags are short because they are derived from path segments.
Does this over-purge?
By design. Purging an ancestor invalidates the whole subtree, which is correct when a change to the parent genuinely affects its descendants — a renamed section appears in every child’s breadcrumb. Purge the narrowest tag that covers the change.
What if the hierarchy is a graph rather than a tree?
Emit one prefix chain per path through the graph. A page reachable by three routes carries three chains, which is still far cheaper than enumerating the URLs affected by a change to any of them.
Related
- Tagging strategies for e-commerce catalog pages covers the flat entity-and-collection scheme this extends.
- Fastly Surrogate-Key invalidation patterns explains the soft-purge option that keeps a large subtree purge from hurting.
- Origin shielding and request collapsing is what absorbs the repopulation spike a subtree purge produces.