ETag and Conditional Requests for Tile Endpoints
On a versioned, immutable tile path an ETag does nothing: the client never revalidates, so the conditional request that would use it is never sent. ETags earn their place on exactly two resources in a tile deployment — the style document and any unversioned alias — and understanding why clarifies the whole caching scheme.
When to Use This
Any path whose content can change while its URL stays the same. In a well-arranged deployment that is a short list: style.json, an unversioned convenience alias like /tiles/latest/, and a dynamically generated tile endpoint where the tile genuinely changes.
Everywhere else, the versioned prefix means the URL changes when the content does, and revalidation is a round trip spent confirming something that cannot have changed.
Specification Detail
| Header | Sent by | Purpose |
|---|---|---|
ETag |
Server | An opaque token identifying this exact body |
Last-Modified |
Server | A weaker, second-resolution alternative |
If-None-Match |
Client | “Send the body only if the ETag differs” |
If-Modified-Since |
Client | The Last-Modified equivalent |
304 Not Modified |
Server | Body unchanged; headers only |
A strong ETag ("abc123") asserts byte equality. A weak one (W/"abc123") asserts semantic equivalence and is what most CDNs generate for compressed responses, since compression changes the bytes without changing the meaning.
Production Configuration
The three path classes, each with the policy that fits it:
# 1. Versioned tiles: immutable, no revalidation, ETag pointless
location ~ ^/v[0-9a-f]+/.+\.(mvt|pmtiles)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
etag off; # nothing will ever ask
root /var/tiles;
}
# 2. The style document: short TTL, revalidation expected
location = /style.json {
add_header Cache-Control "public, max-age=60, must-revalidate";
etag on;
root /var/tiles;
}
# 3. An unversioned alias: revalidate every time, serve 304 when unchanged
location ^~ /tiles/latest/ {
add_header Cache-Control "public, max-age=0, must-revalidate";
etag on;
root /var/tiles;
}
And confirming the behaviour rather than assuming it:
# Capture the ETag, then ask again with it
ETAG=$(curl -sI https://tiles.example.com/style.json | awk '/^etag:/ {print $2}' | tr -d '\r')
curl -sI -H "If-None-Match: $ETAG" https://tiles.example.com/style.json | head -1
# HTTP/2 304
# A versioned tile should not offer one at all
curl -sI https://tiles.example.com/v43/basemap/12/2048/1362.mvt | grep -ci etag
# 0
Why Versioned Tiles Should Not Carry an ETag
It is harmless but misleading, and it invites a specific mistake. A team looking at a dashboard of 304 responses on tile paths concludes that revalidation is working well, when what it actually indicates is that immutable is missing and every reload is costing forty round trips.
There is also a subtler cost. Generating a strong ETag requires hashing the body, which for a tile server producing tiles on the fly is work done per request for a header nobody will use. Turning it off on immutable paths removes both the hashing and the confusion.
When ETags Actively Help
The style document. Fetched on every page load, changes on every deploy, and is small. A 304 costs a round trip and saves a few kilobytes — worthwhile, and more importantly it is what makes a sixty-second TTL cheap enough to keep.
An unversioned alias. Some consumers want a stable URL — a desktop GIS bookmark, an embed someone else maintains. Serving /tiles/latest/ with must-revalidate and an ETag gives them correctness at the cost of a round trip per request, which is the honest trade for a URL whose content changes.
A dynamic tile endpoint. Where tiles are generated from a database, an ETag derived from the underlying data’s last-modified timestamp lets a client skip the body when nothing changed. The saving is real when tiles are large and updates are infrequent.
Interaction Effects
With immutable. The two are mutually exclusive in practice. immutable tells the client never to revalidate; an ETag is only used during revalidation. A path with both is a path where one of them is wrong.
With compression. A CDN that compresses on the fly usually produces a weak ETag, because the compressed bytes differ from the origin’s. That is correct behaviour, and it means If-None-Match still works while byte-for-byte comparison does not.
With stale-while-revalidate. A better fit than a bare ETag for the style document: the client gets the stale copy immediately and the revalidation happens in the background, so the round trip is off the critical path entirely.
With versioned rotation. The reason ETags matter so little here is that versioning already solves the problem they address. A URL that changes with its content needs no mechanism for detecting that its content changed.
Performance Impact
| Resource | Policy | Requests per reload | Bytes |
|---|---|---|---|
| 40 versioned tiles | immutable |
0 | 0 |
| 40 versioned tiles | max-age + ETag |
40 × 304 | ~8 KB of headers |
| style.json | max-age=60 + ETag |
1 × 304 | ~200 B |
| style.json | no caching | 1 full | ~40 KB |
The second row is the case worth avoiding, and it is the default a deployment falls into when immutable is omitted. The third and fourth show why the style document is where conditional requests genuinely pay.
Common Mistakes
Setting immutable and an ETag on the same path. One of them is doing nothing; usually it means immutable was added without removing the revalidation configuration.
Reading a high 304 rate on tiles as success. It means the immutable directive is missing.
Generating strong ETags on a dynamic tile server. Hashing every response body for a header that immutable clients ignore.
Relying on Last-Modified alone. Second resolution is too coarse for a resource that can change twice in a second, which a style document during a deploy can.
FAQ
Should tiles ever revalidate?
Only on an unversioned path. With a version prefix the URL is the version, so a tile at a given URL cannot change and revalidation has nothing to discover.
Is a weak ETag good enough?
For revalidation, yes — it is exactly what weak ETags are for. Strong ETags matter for range requests and byte-exact comparisons, neither of which applies to a style document.
What about stale-while-revalidate instead?
Usually better for the style document. Readers get an immediate response and the revalidation happens in the background, which removes the round trip from the critical path.
Do PMTiles range requests use ETags?
They can, and the important property is stability: an ETag that changes between range requests to the same object suggests the object was replaced mid-read, which is a 416 waiting to happen.
Related
- CDN Cache Headers & Versioned Tile URLs — the parent topic and the three path classes.
- Cache-Control Headers for Immutable Vector Tiles — the directive that makes revalidation unnecessary.
- Versioned Tile URL Rotation Without Cache Purges — why the URL changing removes the need for an ETag.
- Measuring Tile Cache Hit Ratio at the Edge — why a revalidated response is not counted as a hit.