Gzip vs Brotli for Vector Tile Payloads
Brotli at a high quality level compresses MVT tiles roughly 12–18% smaller than gzip and costs several times more CPU to produce. For tiles that are compressed once at build time and served millions of times, that trade is almost always worth taking. For tiles generated per request from a database, it usually is not.
When to Use Each
The deciding question is how many times a given tile is compressed relative to how many times it is served.
Pre-compressed static tiles. A PMTiles archive or a directory of .mvt files is compressed once during the build. Brotli quality 11 is affordable there because the cost is paid once, offline, and every byte saved is saved on every request afterwards.
Tiles generated on demand. A tile server producing MVT from PostGIS compresses each response. Brotli at quality 11 can cost more CPU than generating the tile did; quality 4–5 is the sensible ceiling, and gzip is often the better answer.
Tiles behind a CDN that compresses for you. Most CDNs will apply their own compression, usually Brotli at a modest quality, to responses served uncompressed. This is convenient and it is the worst of both worlds for pre-built archives — see the interaction section below, because for PMTiles it actively breaks range reads.
Specification Detail
| gzip | Brotli | |
|---|---|---|
Content-Encoding |
gzip |
br |
| Quality range | 1–9 | 0–11 |
| Typical build setting | 6 | 11 |
| Typical on-the-fly setting | 6 | 4 |
| Browser support | Universal | Universal over HTTPS |
| Window size | 32 KB | up to 16 MB |
Brotli’s advantage on MVT comes mostly from its larger window and its static dictionary. Vector tiles are full of repeated attribute strings and near-identical varint runs, and a 16 MB window sees repetitions across a whole tile that gzip’s 32 KB window cannot.
Production Command
Compressing a directory of tiles at build time, keeping both encodings so the server can content-negotiate:
# Brotli for clients that accept it, gzip as the universal fallback
find tiles/v43 -name '*.mvt' -print0 | xargs -0 -P "$(nproc)" -I{} sh -c '
brotli -q 11 -f -k "{}" -o "{}.br"
gzip -9 -f -k -c "{}" > "{}.gz"
'
# Nginx then serves whichever the client asked for, with no runtime cost
# gzip_static on;
# brotli_static on;
For a PMTiles archive the compression is per tile inside the archive, and the codec is recorded in the header. Tippecanoe writes gzip; converting the internal compression is not something to do by hand:
pmtiles show basemap.pmtiles | jq '{tile_compression, tile_type}'
# { "tile_compression": "gzip", "tile_type": "mvt" }
Interaction Effects
With PMTiles range requests. This is the important one. A PMTiles archive’s directory addresses tiles by byte offset, and any transformation of the response body invalidates every offset at once. A CDN that recompresses the archive turns range reads into garbage. Serve .pmtiles with Content-Type: application/octet-stream and confirm the edge is not applying its own compression — the range-request delivery topic covers the whole checklist.
With double compression. Tiles stored gzipped and served with Content-Encoding: gzip are correct. Tiles stored gzipped and also compressed by the server are double-encoded and decode to noise — see diagnosing double-gzipped tiles.
With attribute filtering. Compression and attribute filtering attack the same bytes from opposite ends, and filtering wins. Removing a high-cardinality column removes data that compresses badly by definition; no codec recovers as much as not shipping it.
Performance Impact
Compression time at build scale is the number that decides the quality level. Measured over a 12,000-tile z0–z14 city build:
| Setting | Total compress time | Archive size |
|---|---|---|
gzip -6 |
41 s | 1.42 GB |
gzip -9 |
2 m 18 s | 1.38 GB |
brotli -q 4 |
1 m 06 s | 1.27 GB |
brotli -q 11 |
24 m 30 s | 1.19 GB |
Decompression is cheap and roughly comparable for both codecs — single-digit milliseconds for a typical tile — so the client-side cost is not a differentiator. The reader-facing win is entirely in transfer size, which matters most on the mobile connections where a 15% saving is a visible fraction of a second.
Common Mistakes
Setting Brotli quality 11 on a dynamic tile server. The compression can take longer than the database query, and under load the CPU cost dominates everything else. Quality 4 gives most of the ratio for a small fraction of the time.
Letting the CDN compress a PMTiles archive. Produces range reads that return valid-looking bytes decoding to nothing. The symptom is a map that works locally and fails only behind the CDN.
Shipping only Brotli. Universal support is over HTTPS with a modern browser; a plain-HTTP development environment or an older client will fall back to uncompressed. Keep a gzip variant.
Measuring the ratio on one tile. Ratios vary widely by zoom and layer mix — a sparse z8 tile and a dense z14 tile compress very differently. Measure over the whole archive before choosing a setting.
FAQ
Should a PMTiles archive be Brotli-compressed internally?
The PMTiles v3 header can record Brotli as the tile compression, but tooling support is narrower than for gzip and the archive is only readable by clients that handle it. Unless you control every consumer, gzip inside the archive with a well-configured CDN in front is the safer arrangement.
Does Brotli decode more slowly on the client?
Not meaningfully for tile-sized payloads. Both codecs decode a typical 100 KB tile in single-digit milliseconds, and the difference is dwarfed by the protobuf parse that follows. The trade is entirely about compression cost and transfer size, not decode cost.
Is it worth serving both encodings?
For static tiles, yes — the extra storage is 8–10% and the server picks whichever the client accepts with no runtime cost. For a dynamic tile server it is not, because each response would have to be compressed twice or cached twice.
Why does the ratio vary so much between zoom levels?
Because the content does. Low-zoom tiles are geometry-dominated with long runs of similar deltas that compress extremely well; high-zoom tiles carry proportionally more distinct attribute strings, which compress poorly. Any single-tile measurement is unrepresentative of the archive.
Related
- MVT Encoding Internals — the parent topic, and why the encoding compresses the way it does.
- Diagnosing Double-Gzipped Tiles — the failure mode most often introduced while changing compression.
- PMTiles Range-Request Delivery — why any body transform breaks an archive.
- Cache-Control Headers for Immutable Vector Tiles — the headers that sit alongside
Content-Encoding.