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.

What each codec and quality level producesThe same 470 KB uncompressed tile compressed with gzip 6, gzip 9, Brotli 4 and Brotli 11, showing diminishing returns above Brotli 4.COMPRESSED SIZEKB, one z14 mixed urban tile (470 KB raw)gzip -6121 KBgzip -9117 KBbrotli -q 4108 KBbrotli -q 11101 KB
Brotli 4 captures most of the win at a fraction of the cost. Quality 11 is for build time, where the extra CPU is paid once.

Production Command

Compressing a directory of tiles at build time, keeping both encodings so the server can content-negotiate:

bash
# 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:

bash
pmtiles show basemap.pmtiles | jq '{tile_compression, tile_type}'
# { "tile_compression": "gzip", "tile_type": "mvt" }
The three places a tile can be compressed, and which one to pickCompression at build time, at the tile server, or at the CDN edge, with the cost and cache implications of each.WHERE IT HAPPENSBuild timebrotli -q 11paid oncethe right answer forstatic tilesTile serverper requestq4-6necessary for dynamictilesCDN edgeautomaticbreaks PMTiles rangereads
Compress once, as early as possible. Every arrangement that compresses per request is paying repeatedly for a result that never changes.

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.

Which codec and quality level fits which stageFour deployment shapes mapped to a compression choice: build-time static tiles, an on-demand tile server, a PMTiles archive and a CDN that compresses automatically.DECIDEHow many times will these bytes be compressedrelative to being served?Once, at build timeBrotli 11 — the cost ispaid once, offlineOnce per requestgzip 6, or Brotli 4 atmostInside a PMTilesarchivegzip, and stop the CDNtransforming the bodyBy the CDN,automaticallyFine for tiles, fatal forarchives

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.