Short answer

PostGIS is a strong web-mapping backend because it can turn a spatial query into a Mapbox Vector Tile (MVT) on the fly with ST_AsMVT, so the database itself becomes the tile server. The browser requests a tile at z/x/y, PostGIS reads only the rows in that tile's envelope through a GiST index, clips and encodes them into a compact binary blob, and a thin HTTP service streams it back. Done well — indexed geometry, per-zoom simplification, and an HTTP cache — a single Postgres instance comfortably drives an interactive map. The failure modes are predictable: missing index, no simplification, and serving full-precision GeoJSON instead of tiles.

The tile request lifecycle

A web map client (MapLibre GL JS, OpenLayers, Leaflet with a vector plugin) divides the world into a quadtree of tiles addressed by zoom z, column x, and row y, using the Web Mercator grid (EPSG:3857). When the user pans, the client requests the tiles now in view. Your backend's job is to answer each z/x/y request with the features inside that tile, at appropriate detail.

The whole pipeline can be one SQL statement:

SELECT ST_AsMVT(tile, 'roads', 4096, 'geom')
FROM (
  SELECT
    id, name, road_class,
    ST_AsMVTGeom(
      ST_Transform(geom, 3857),
      ST_TileEnvelope(:z, :x, :y),
      4096, 64, true
    ) AS geom
  FROM roads
  WHERE ST_Transform(geom, 3857) && ST_TileEnvelope(:z, :x, :y)
) AS tile
WHERE tile.geom IS NOT NULL;

Reading inside-out: ST_TileEnvelope(z,x,y) builds the tile's bounding box in EPSG:3857. The && bounding-box operator, backed by a spatial index, filters to candidate rows. ST_AsMVTGeom clips each geometry to the tile, snaps it to the tile's integer coordinate grid (4096 extent, 64-unit buffer to avoid edge clipping artefacts, true to clip), and discards anything fully outside. ST_AsMVT aggregates the survivors into one binary tile in a layer named roads. The result is a bytea you return with Content-Type: application/vnd.mapbox-vector-tile.

Why MVT beats serving GeoJSON

The naive backend serves ST_AsGeoJSON of a whole layer. It works at small scale and falls apart fast:

  • Payload size. GeoJSON is verbose text with full-precision coordinates. A coastline that is 40 MB of GeoJSON is a few hundred kilobytes of MVT once clipped and quantised to a tile.
  • Detail control. A tile is naturally scale-limited — you only send what is in view at that zoom. A whole-layer GeoJSON sends everything regardless of the viewport.
  • Client cost. The browser parses and indexes MVT efficiently; megabytes of JSON block the main thread.

GeoJSON remains the right answer for small, dynamic overlays (a handful of editable features, a query result of a few hundred rows). For base layers and large datasets, tiles win decisively.

Serving the tiles: build vs buy

You rarely call PostGIS directly from the browser. Two common patterns:

pg_tileserv (from the PostGIS/Crunchy ecosystem) auto-publishes every spatial table as an MVT endpoint at /{schema}.{table}/{z}/{x}/{y}.pbf, running the ST_AsMVT query for you. It is the fastest way to a working vector-tile service and supports function layers for parameterised queries.

A custom API (a small Node, Python/FastAPI, or Go service) is worth it when you need auth, per-tenant filtering, computed attributes, or to merge several tables into one tile. The service just substitutes z/x/y into the query above and returns the bytea.

Either way, put an HTTP cache (a CDN, or Nginx with proxy_cache) in front, keyed on the tile path. Most tile requests repeat, and caching turns a database hit into a static file response.

Live tiles vs pre-rendering

Generate tiles on demand when data changes and traffic is moderate — most internal dashboards and client portals. Pre-render to static .pbf files or an MBTiles archive when data is static, traffic is high, or low-zoom tiles must aggregate millions of features (those queries are expensive every time). GDAL can pre-bake a tile pyramid:

ogr2ogr -f MVT tiles_out data.gpkg \
  -dsco MINZOOM=0 -dsco MAXZOOM=14 \
  -dsco COMPRESS=YES

A common hybrid: pre-render stable base layers, serve volatile layers live, and overlay them in the same map.

Performance: the four levers

  1. Spatial index. CREATE INDEX roads_geom_gix ON roads USING GIST (geom); is non-negotiable. Without it every tile triggers a full scan. If your data is stored in 4326 but tiled in 3857, index a generated 3857 column or a functional index so the && filter stays sargable rather than transforming every row.

  2. Per-zoom simplification. A 1 m-accurate geometry is wasted at zoom 4, where one pixel covers kilometres. Simplify by zoom:

ST_AsMVTGeom(
  ST_Simplify(ST_Transform(geom, 3857), tolerance_for(:z)),
  ST_TileEnvelope(:z, :x, :y), 4096, 64, true)

Use a tolerance roughly equal to one tile-pixel in metres at that zoom (Web Mercator ground resolution at the equator is about 156543 / 2^z metres per pixel). Pre-compute simplified columns for the heaviest layers rather than simplifying on every request.

  1. Feature limits at low zoom. Cap features (LIMIT, or filter by an importance/road_class attribute) so zoomed-out tiles do not try to encode an entire country.

  2. HTTP caching. Cache by z/x/y. This is the single biggest win under real traffic.

Always profile with EXPLAIN (ANALYZE, BUFFERS) on a representative low-zoom tile — that is where queries blow up — and confirm the GiST index appears in the plan.

Validation before you ship

  • Request a known tile and confirm a non-empty bytea and the right content type.
  • Decode a tile (ogrinfo reads MVT) and check the expected layer name and feature count.
  • Verify alignment: features should not visibly jump at tile boundaries (a buffer too small in ST_AsMVTGeom causes hairline gaps).
  • Load-test a low-zoom tile cold; that is your worst case.

Bathyl perspective

We use PostGIS as a tile backend when a map needs to be live, queryable, and tied to data that changes — not a frozen export. The discipline is the same as any data product: index the geometry, control detail per zoom, cache aggressively, and keep one authoritative table behind the service so the web map and the analysis read the same truth.

Related reading

Sources