EPSG:3857 vs EPSG:4326 for Tile Delivery
The two CRS are not competitors: one is how a spatial estate stores data and the other is how it delivers tiles, and a domain that confuses the roles ends up either reprojecting on every request or shipping a tile grid its consumers’ clients cannot use. This guide sets out which belongs where, what each costs at the delivery boundary, and how to serve both without keeping two copies of the data. It applies the CRS conventions from CRS Governance and Reprojection Standards within Geospatial Data Mesh Fundamentals to the tile path described in Edge Caching and Tile Delivery Topology.
Prerequisites
| Requirement | Value / Assumption | Notes |
|---|---|---|
| Tools | PostGIS ≥ 3.3, tippecanoe or an equivalent tiler, ogrinfo |
ST_AsMVT does the projection at query time |
| Storage | Canonical storage in EPSG:4326 |
Delivery CRS is a port concern, not a storage one |
| Matrix sets | WebMercatorQuad (3857) and WorldCRS84Quad (4326) |
Both are OGC-registered |
| Access roles | domain-owner declares the published matrix sets |
It is a contract decision |
| Environment | PRODUCT, PG_DSN |
Exported before running |
Step-by-Step Implementation
1. Decide from the consumer, not from the data
| Consumer | Expects | Matrix set | Why |
|---|---|---|---|
| Web map client | Web Mercator | WebMercatorQuad |
Every major web mapping library assumes it |
| Desktop GIS | Either; usually geographic | WorldCRS84Quad |
No Mercator assumption; polar data is usable |
| Polar or high-latitude work | Neither | A polar matrix set | Mercator is undefined beyond ±85.05° |
| Analytical consumer | No tiles at all | — | A snapshot port, not a tile port |
The high-latitude row is a genuine constraint rather than a preference: EPSG:3857 is mathematically undefined outside roughly ±85.05° latitude, so a domain publishing Arctic or Antarctic coverage cannot serve it through a Web Mercator pyramid at all.
2. Serve Web Mercator tiles from geographic storage
ST_AsMVT projects at query time, so a single EPSG:4326 store serves a EPSG:3857 pyramid without a second copy.
-- mvt_tile.sql — a WebMercatorQuad tile, from EPSG:4326 storage.
-- Parameters: :z, :x, :y (integers)
WITH bounds AS (
-- The tile's envelope in EPSG:3857, then back to 4326 for the indexed lookup.
SELECT ST_TileEnvelope(:z, :x, :y) AS env_3857,
ST_Transform(ST_TileEnvelope(:z, :x, :y), 4326) AS env_4326
),
src AS (
SELECT f.feature_id, f.attrs,
-- Project only the features that survive the indexed 4326 filter.
ST_AsMVTGeom(
ST_Transform(f.geom, 3857),
b.env_3857,
4096, -- tile extent in integer units
64, -- buffer, so features crossing the edge render cleanly
true -- clip to the tile
) AS geom
FROM product_features f, bounds b
-- && against the 4326 envelope uses the GiST index; transforming the COLUMN
-- instead would make the index unreachable and scan the table.
WHERE f.product = :'product' AND f.geom && b.env_4326
)
SELECT ST_AsMVT(src, 'features', 4096, 'geom') FROM src WHERE geom IS NOT NULL;
Verify the index is reachable — the difference between a tile served in milliseconds and one that scans the layer:
psql "$PG_DSN" -c "EXPLAIN (ANALYZE) $(cat mvt_tile.sql)" | grep -E 'Index Scan|Seq Scan'
# "Index Scan using ..._geom_idx" is correct. "Seq Scan" means the filter transformed
# the column rather than the envelope.
3. Publish both matrix sets where consumers need both
# product.yaml — two tile ports from one materialization.
spec:
storage:
crs: "EPSG:4326" # one canonical store
ports:
- type: tiles
matrixSet: WebMercatorQuad # EPSG:3857 — web clients
zoom: { min: 6, max: 16 }
- type: tiles
matrixSet: WorldCRS84Quad # EPSG:4326 — desktop GIS, polar-safe
zoom: { min: 4, max: 14 }
Verify that the matrix set is part of the cache key, or the two pyramids will overwrite each other — they share z/x/y coordinates and are entirely different tiles:
a=$(curl -sS "https://$TILE_HOST/$PRODUCT/WebMercatorQuad/8/128/85.mvt" | sha256sum)
b=$(curl -sS "https://$TILE_HOST/$PRODUCT/WorldCRS84Quad/8/128/85.mvt" | sha256sum)
[ "$a" != "$b" ] && echo "matrix sets are distinguished" || echo "CONTAMINATION"
4. Measure what the projection actually costs
# Projection at query time is cheap relative to the spatial filter, but measure it
# rather than assuming — it scales with feature count, not with tile count.
psql "$PG_DSN" -c "\timing on" -c "$(cat mvt_tile.sql)" > /dev/null
Configuration Reference
| Parameter | Value | Effect |
|---|---|---|
| Storage CRS | EPSG:4326 |
One canonical copy; interchange everywhere |
WebMercatorQuad |
EPSG:3857 |
Web clients; undefined beyond ±85.05° |
WorldCRS84Quad |
EPSG:4326 |
Desktop GIS; usable at all latitudes |
| MVT extent | 4096 |
Integer tile units; standard for vector tiles |
| MVT buffer | 64 |
Features crossing the tile edge render without seams |
| Filter envelope | Transformed to 4326 |
Keeps the GiST index reachable |
| Cache key | Includes matrix set | Two pyramids share z/x/y and must not collide |
Common Failure Modes & Fixes
Tiles are correct and the query scans the whole table. Root cause: the filter transforms the geometry column rather than the tile envelope, so the index cannot be used. Fix: transform the envelope into the column’s CRS, as above.
Features are clipped visibly at tile boundaries.
Root cause: ST_AsMVTGeom buffer of zero, so a feature crossing the edge is cut exactly at it and the two halves do not join. Fix: a buffer of 64 units; the renderer discards the overlap.
Nothing renders above about 85° latitude.
Root cause: EPSG:3857 is undefined there — this is not a bug. Fix: publish a polar matrix set for high-latitude coverage, and state the Web Mercator port’s latitude limit in the contract.
Distances or areas computed from delivered tiles are wrong.
Root cause: measuring in EPSG:3857, where scale distortion grows with latitude and area is badly wrong away from the equator. Fix: measurement belongs against the EPSG:4326 store using geography, never against a delivery pyramid.
The two matrix sets return each other’s tiles. Root cause: matrix set absent from the cache key. Fix: add it; the symptom is misaligned geometry at some zoom levels and not others, which makes it easy to misdiagnose as a tiler bug.
FAQ
Should a domain ever store data in EPSG:3857?
Only if every consumer is a web map and nothing ever measures anything, which is rarer than it sounds. Web Mercator’s distortion makes area and distance computed directly in it wrong by a factor that grows with latitude, so any analytical use of the store produces quietly incorrect answers. Storing in EPSG:4326 and projecting at delivery costs a small amount of query-time CPU and keeps the store correct for everything else. The one exception is a pre-rendered raster tile pyramid, which is delivery output rather than storage.
Is projecting at query time expensive enough to matter?
Less than teams expect, because it scales with the features in the tile rather than with the tiles served, and the spatial filter that precedes it eliminates the vast majority of the layer first. In practice the dominant cost in a vector tile query is the indexed lookup and the MVT encoding, with the projection a small fraction of the total. Where profiling shows otherwise, the answer is usually a precomputed pyramid for the hot zoom levels rather than a second store in a different CRS.
What about EPSG:4326 tiles with a 2:1 root?
That is WorldCRS84Quad, and the two-tile root is exactly what makes it geographically honest: the world in degrees is twice as wide as it is tall. Web clients that hard-code a single square root tile will not handle it, which is precisely why it is a second port rather than a replacement. Desktop GIS and OGC-conformant clients handle it natively, and it remains usable at latitudes where Web Mercator does not exist.
Does the delivery CRS belong in the product version identifier?
Only for the storage CRS, which is what the -crs: component records. Delivery matrix sets are port properties rather than artifact properties: the same materialization serves both pyramids, so encoding a delivery CRS in the version would imply two artifacts where there is one. The port declaration in the manifest is where a consumer looks to find which pyramids exist, and the cache key is where they are kept apart.
Should a domain precompute both pyramids or render on demand?
Precompute the zoom levels demand actually reaches and render the rest on request, for both matrix sets. Precomputing a full pyramid to maximum zoom across a national extent generates enormous numbers of tiles that will never be requested, and doing it twice for two matrix sets doubles that waste. Measuring the observed zoom distribution and precomputing only down to where demand becomes sparse cuts pipeline cost and storage substantially with no consumer-visible difference, because the rare deep-zoom tile is rendered once on first request and cached thereafter.
Does serving two matrix sets double the cache footprint?
It roughly doubles the objects for the extents both are actually requested over, which is usually far less than double overall — the two matrix sets typically serve different consumer populations with different areas of interest. The more important consideration is that both pyramids must be in the cache key, since they share z/x/y coordinates and would otherwise overwrite each other. Where cache capacity is genuinely constrained, the honest question is whether the second matrix set has consumers, and retiring an unused port is better than under-caching both.
Related
- CRS Governance and Reprojection Standards — the parent topic and the CRS conventions
- Edge Caching and Tile Delivery Topology — why the matrix set must be in the cache key
- Standardizing on EPSG:4326 Across Domains — the canonical storage decision