Versioned Tile URL Rotation Without Cache Purges

Embed a schema hash in the tile path prefix, publish each rebuild to that fresh path, flip the style’s pointer to it, and never purge the CDN. Because the old prefix is still valid and the new prefix is what the style now names, a deploy is atomic and a global cache purge — the slow, stampede-inducing operation you are trying to avoid — is never required.

When to Use This

Use prefix rotation whenever your tiles are cached immutably and rebuilt on a schedule: a nightly OpenStreetMap extract, a weekly parcel refresh, a CI job that regenerates tiles on data change. The moment you commit to public, max-age=31536000, immutable on tile paths — the header described in Cache-Control headers for immutable vector tiles — you also commit to never overwriting a path in place, because an immutable object cannot be updated by re-upload; caches will ignore the new bytes. Rotation is the mechanism that makes immutable caching and frequent rebuilds coexist.

If your tiles are served with a short TTL and no immutable directive, you do not need this — a normal purge or natural expiry suffices. Rotation earns its keep precisely when the cache lifetime is long.

Specification Detail

The four inputs that decide a version prefixSource data snapshot, build flags, tool version and layer schema, combined into the hash that names the published prefix.WHAT GOES IN THE HASHsource snapshotcontent hashthe data itself, not itstimestampbuild flagsexact stringa changed -z producesdifferent tilestippecanoe versionpinnedencoders change betweenreleaseslayer schemanames + typesso a rename gets a newprefix
Anything that changes the bytes of a tile must be in the hash. Leave the flag set out and a re-tiled build publishes to the same prefix as the old one.

Computing a stable schema hash. The prefix must be deterministic: identical inputs must produce the same hash so an unchanged rebuild does not needlessly rotate, and any schema or data change must produce a new one. Hash the tileset metadata — the PMTiles header plus its tile-JSON (layer names, fields, min/max zoom, bounds) — not the wall-clock time.

Input to the hash Why it belongs
Layer names + field lists A renamed layer must force a new prefix so it cannot mix with an old style
min/max zoom, bounds Coverage changes should invalidate old tiles
Content digest of the archive Any byte change in the tiles rotates the prefix
Build timestamp Excluded — including it defeats the “unchanged rebuild = same prefix” property

Path layout. The prefix sits at the front of the path so it is part of the cache key and trivially matched by CDN and lifecycle rules:

text
/v{hash}/{z}/{x}/{y}.pbf         # dynamic-style tile paths
/v{hash}/roads.pmtiles           # single-archive layout

A short digest (the first 8 hex characters of a SHA-1 over the metadata) is enough — collision probability is negligible at the handful of builds a prefix ever needs to disambiguate.

Production Command

A complete rebuild-and-rotate step, suitable for a CI job:

bash
#!/usr/bin/env bash
set -euo pipefail

BUCKET="s3://tiles.example.com"
STYLE_KEY="style.json"

# 1) Derive a stable prefix from tileset metadata (not the clock)
HASH=$(pmtiles show roads.pmtiles --header-json \
  | python3 -c "import sys,hashlib;print(hashlib.sha1(sys.stdin.buffer.read()).hexdigest()[:8])")
PREFIX="v${HASH}"
echo "Publishing under /${PREFIX}/"

# 2) Upload to the prefixed path with an immutable header
aws s3 cp roads.pmtiles "${BUCKET}/${PREFIX}/roads.pmtiles" \
  --content-type application/octet-stream \
  --cache-control "public, max-age=31536000, immutable"

# 3) Pre-warm the busiest low-zoom tiles so the flip does not cold-start every POP
for z in 0 1 2 3 4; do
  curl -s -o /dev/null "https://tiles.example.com/${PREFIX}/roads.pmtiles" \
    -H "Range: bytes=0-16383"   # PMTiles header + root directory
done

# 4) Template the new prefix into the style's sources.url and publish it
python3 - "$PREFIX" <<'PY'
import json, sys
prefix = sys.argv[1]
style = json.load(open("style.template.json"))
style["sources"]["roads"]["url"] = f"pmtiles://https://tiles.example.com/{prefix}/roads.pmtiles"
json.dump(style, open("style.json", "w"), separators=(",", ":"))
PY

# 5) Flip the pointer — the ONLY mutable write, on a short TTL
aws s3 cp style.json "${BUCKET}/${STYLE_KEY}" \
  --content-type application/json \
  --cache-control "public, max-age=300, stale-while-revalidate=60"

echo "Live on /${PREFIX}/ — no purge issued"

Step 5 is the atomic flip: the style is the single mutable object, and because it carries a 5-minute TTL, clients pick up the new prefix within minutes while old tiles keep serving from cache until then.

Interaction Effects

Pairs with immutable Cache-Control. Rotation and the immutable header from the sibling guide are two halves of one design. Immutable caching gives you the hit ratio and the zero-revalidation wins; rotation gives you a way to ship new data despite those bytes being frozen forever. Neither works alone — immutable without rotation means you can never update, and rotation without immutable means you gave up the cache benefit you rotated to protect.

Dual-deploy window. Between the tile upload (step 2) and the style flip (step 5), both the old and new prefixes are live and valid. That overlap is a feature: in-flight clients on the old style keep hitting the old prefix with no errors, and there is no instant during which a client can request a tile that does not exist. You can widen the window deliberately — hold both prefixes for a release cycle — to support gradual rollout or fast rollback (flip the style back to the old prefix, still warm in cache).

Ties to the style version-pair gate. The correctness of the flip depends on the tiles and the style being a matched pair: a tile prefix names a schema, and the style that consumes it must reference the same schema’s layer names and fields. Treating the (prefix, style) pair as the unit of deploy — never updating one without the other — is what guarantees a new style never lands against old-schema tiles.

Performance Impact

How readers migrate to a new version, and how long the old one must liveThe rollout window from publishing the new prefix through the style TTL expiring for the last reader, with the safe deletion point for the old prefix.ROLLOUT60 s style TTL1341New prefix uploaded — 5 s2Style rewritten — 1 s3Readers refetch as their style expires — 0-60 s4Old prefix retained for rollback — 24 h
The old prefix must outlive the longest cached style, not the average one. A 60-second TTL and a 24-hour retention is a comfortable ratio.
  • No purge latency. A global CDN purge propagates over seconds to minutes and cold-starts every POP; rotation replaces it with a single small-object write (the style) that costs nothing at the edge. Deploy time drops to the time to upload one JSON file.
  • Old prefixes age out naturally. Because nothing points at the old prefix after the flip, its edge entries simply expire on their own TTL and its origin objects sit idle until a lifecycle rule removes them — no active invalidation, no stampede.
  • Bounded origin egress. Misses occur only for the new prefix on first access; pre-warming the low-zoom tiles collapses even that into a handful of origin fetches. A busy basemap can rotate nightly and still run origin egress in the low single-digit percent of total traffic.

Common Mistakes

What has to hold for rotation to replace purgingFive conditions: the hash covers every input, the style TTL is short, the old prefix survives long enough, the style is not itself immutable, and rollback is a pointer change.ROTATION CHECKSThe hash covers data, flags, tool version and schemaotherwise two different builds share a prefixstyle.json TTL is short — 60 seconds or soit sets how long a rollout takesThe previous prefix is retained well past that TTLdeleting it early blanks the map for anyone still on the old stylestyle.json is never marked immutableit is the one document that must change in placeRollback is rewriting the style, not restoring tilesthe old prefix is still there and still warm
The fourth row is the one that bites: marking style.json immutable pins every reader to the old version prefix for a year.

1. Reusing a prefix. Publishing a changed build under a prefix that already served different bytes is the cardinal error — immutable caches will keep serving the old bytes, and any client that fetches fresh gets a schema that may not match. The symptom is the “mixed old/new schema” failure: some layers render, others throw source layer "..." does not exist. Fix: derive the prefix from a content digest so different bytes can never collide onto the same path.

2. Forgetting to update the style’s sources.url. If you upload to a new prefix but never flip the pointer, clients keep loading the old prefix indefinitely — the deploy silently does nothing.

bash
# Confirm the live style actually names the new prefix:
curl -s "https://tiles.example.com/style.json" \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['sources']['roads']['url'])"
# pmtiles://https://tiles.example.com/va1b2c3d4/roads.pmtiles

If the printed prefix is not the one you just published, step 5 did not run.

3. Unbounded prefix accumulation. Every rebuild leaves an old prefix on the origin; a nightly job produces 365 dead prefixes a year, and object-storage bills grow accordingly. Add a lifecycle rule to delete prefixes past a retention window — long enough to cover rollback and the longest client cache lifetime, typically 7–30 days.

bash
aws s3api put-bucket-lifecycle-configuration \
  --bucket tiles.example.com \
  --lifecycle-configuration '{"Rules":[{"ID":"expire-old-tile-prefixes",
    "Filter":{"Prefix":"v"},"Status":"Enabled",
    "Expiration":{"Days":30}}]}'

Parent: CDN Cache Headers & Versioned Tile URLs

FAQ

What goes into the version hash?

Everything that changes the tile bytes: the source snapshot, the build flags, the tool version and the layer schema. Leaving the flag set out means a re-tiled build publishes to the same prefix as the old one.

How long must the old prefix be kept?

Longer than the longest cached style, plus a rollback margin. With a sixty-second style TTL, retaining for a day is generous and costs almost nothing in cold storage.

How does rollback work?

Rewrite the style to name the previous prefix. Nothing is rebuilt and nothing is purged, because the old tiles are still there and still warm.

Does this work for tile servers as well as static archives?

Yes. The prefix is part of the URL path, so a tile server that includes it in its routing gets the same properties without changing anything else.