Blog / Vector tiles

Tutorials

What Are Vector Tiles? A Complete Guide For Developers

Modern maps feel fast because they do not ship one giant map image to the browser. They ship small pieces of geographic data, just enough for the current zoom level and viewport, and let the client render the final map.

June 4, 2026 / 22 min read / 2 embedded videos / By Jenish Hapaliya and Nandan Padia

Modern maps feel fast because they do not ship one giant map image to the browser. They ship small pieces of geographic data, just enough for the current zoom level and viewport, and let the client render the final map.

That is the idea behind vector tiles.

A vector tile is a small packet of map data for one square of the world at one zoom level. Instead of containing pixels, it contains geometry: points for places, lines for roads and rivers, polygons for buildings, landuse, water, boundaries, and any attributes needed to style or query those features.

If you are building a web map, fleet dashboard, logistics tool, real estate search experience, civic data portal, marine map, field operations app, or anything with interactive geospatial data, vector tiles are usually the format you want. They are compact, styleable, queryable, and built for high zoom map interaction.

This guide explains vector tiles from the developer’s point of view: what they are, how they work, how they compare with raster tiles, how to generate them, how to serve them, how to style them in MapLibre GL JS, and how to debug the problems that usually waste hours.

What are Vector tiles?

Vector tiles are tiled geographic data files, usually encoded as Mapbox Vector Tiles (MVT), that contain simplified vector geometries and attributes for a specific {z}/{x}/{y} tile coordinate.

How vector tiles split map data into zoom-aware packets.

A tile URL usually looks like this:

https://tiles.example.com/{z}/{x}/{y}.mvt

Where:

  • z is the zoom level.
  • x is the tile column.
  • y is the tile row.

At zoom 0, the entire world fits into one tile. At zoom 1, the world is split into 4 tiles. At zoom 2, it is split into 16 tiles. Every zoom level doubles the grid in both directions.

The browser asks for only the tiles needed to fill the visible map area. Pan east, and it fetches the next column. Zoom in, and it fetches tiles for the more detailed zoom level.

Vector Tiles vs Raster Tiles

Raster tiles are images. Vector tiles are data.

That one difference changes almost everything.

A Raster Tile is a pre-rendered PNG, JPEG, or WebP image. The server decides how the roads, labels, buildings, boundaries, and water should look. The client displays the image as-is.

A vector tile is a binary data file. The server sends geometries and attributes. The client decides how to draw them.

AreaRaster tilesVector tiles
File contentsPixelsGeometry and attributes
Common extensions.png, .jpg, .webp.mvt, .pbf
StylingFixed on serverControlled on client
Retina supportNeeds larger imagesRenderer draws sharp at any pixel density
InteractivityLimited to separate metadataQuery features directly in the rendered map
BandwidthCan be high across many styles and densitiesUsually lower for dense maps, especially with reuse
Server costRendered ahead of time or on requestTile generation can be static or dynamic
Client costLowHigher; client decodes and draws geometries
Best forSatellite imagery, static base maps, simple legacy mapsInteractive maps, custom styling, large datasets, dynamic layer control

Use raster tiles when the source is naturally raster: satellite imagery, scanned historical maps, heatmaps rendered as images, or a simple basemap where you do not need runtime styling.

Use vector tiles when users need to click features, change styles, hide layers, switch themes, render sharp maps on high DPI screens, or interact with a large dataset without downloading the whole thing.

A practical rule: if your map is just a background image, raster can be fine. If your map is part of the product experience, use vector tiles.

Why Vector tiles became the default for modern web maps

Early web maps mostly used raster tiles. Google Maps made the tiled map pattern mainstream: break the world into small square images, fetch the visible tiles, stitch them together in the browser, and cache aggressively.

That model scaled well, but it had limits.

If you wanted a dark theme, the provider had to render a second full tile set. If you wanted a high contrast accessibility theme, another tile set. If you wanted labels in a different language, another tile set. If you wanted to hide buildings or emphasize truck routes, the server had to render another style.

Raster tiles also blur on high DPI screens unless you ship larger image tiles, which increases bandwidth.

Vector tiles fix that by moving styling to the client. The server sends reusable map data. The browser renders it using a style JSON. Change the style JSON, and the same tile data can become a Light map, Dark map, Logistics map, Nautical map, Real Estate map, or Fleet operations map.

Mapbox popularized this approach with the Mapbox Vector Tile format. The MVT format then became the practical standard used across Mapbox GL JS, MapLibre GL JS, OpenMapTiles, Martin, TileServer GL, Tegola, PostGIS tile pipelines, and many hosted tile APIs.

How the tile pyramid works

Most web maps use a global tile pyramid. The world is projected into Web Mercator, then split into square tiles at each zoom level.

The common URL pattern is:

/{z}/{x}/{y}

At zoom z, the grid has:

2^z columns
2^z rows

So:

ZoomGrid sizeTotal tilesTypical meaning
01 x 11Whole world
12 x 24Continents
532 x 321,024Countries / large regions
101,024 x 1,0241,048,576Cities / metro areas
1416,384 x 16,384268M possible tilesNeighborhood / street detail
1665,536 x 65,5364.2B possible tilesBuildings / parcels / detailed streets

You do not generate every possible tile. Most datasets cover only part of the world, and many tiles are empty.

The important developer detail is this: every tile should contain the right amount of detail for its zoom level. A zoom 5 tile should not include every tiny residential road and building footprint. A zoom 15 tile can.

If you put too much data into low zoom tiles, the map becomes slow. If you simplify too aggressively at high zoom, the map looks broken.

What lives inside a vector tile

A vector tile usually contains multiple named layers. Each layer contains features. Each feature has geometry and properties.

How named layers and features live inside a vector tile.

A typical basemap tile might include layers like:

road
water
building
landuse
place
boundary
poi
transportation_name
water_name

A custom business dataset might include:

stores
delivery_zones
fleet_positions
risk_areas
service_boundaries
parcels
coverage_cells

Each feature has one geometry type:

  • Point: POIs, labels, store locations, sensors, incidents.
  • LineString: roads, paths, routes, rivers, cables, pipelines.
  • Polygon: buildings, lakes, parks, administrative boundaries, geofences.

Each feature can also carry attributes:

{
  "class": "primary",
  "name": "MG Road",
  "oneway": true,
  "speed_limit": 50
}

Those attributes are what make vector tiles useful. Your style can say: draw primary roads thicker than residential roads, color parks green, hide minor labels below zoom 12, show buildings only after zoom 13, or color delivery zones by SLA risk.

Vector tile coordinates are not Latitude and Longitude

This trips up many developers.

Inside an MVT file, coordinates are not stored as latitude and longitude. They are stored in a local tile coordinate system.

A common tile extent is 4096. That means every tile has an internal coordinate space from 0 to 4096 in both x and y directions.

A line inside a tile might be encoded as points like this:

(220, 1300) -> (450, 1390) -> (900, 1500)

Those numbers are positions inside the tile, not global coordinates.

Why do this?

Because it is compact and efficient. The client already knows where the tile sits in the global map. The tile only needs to describe geometry relative to its own square. MVT also uses integer coordinates and delta encoding, which keeps the binary small.

The pipeline looks like this:

Vector tile encoding pipeline
01

Start with source geometry in lon/lat or projected coordinates

02

Clip geometry to the requested tile bounds

03

Simplify geometry for the target zoom level

04

Transform coordinates into the tile extent, usually 4096 units

05

Encode the result as MVT protobuf

06

Let the client decode, style, and render the final map

The browser rendering pipeline

When a MapLibre or Mapbox GL map displays vector tiles, it roughly does this:

Browser rendering pipeline
01

Calculate visible tile coordinates for the current viewport

02

Request the needed .mvt or .pbf files

03

Decode protobuf tile data in worker threads

04

Parse the map style JSON

05

Match style layers to source layers inside the tiles

06

Convert matching features into GPU buffers

07

Draw fills, lines, circles, symbols, and 3D extrusions with WebGL

08

Re-render on pan, zoom, pitch, bearing, hover, or style change

This is why vector tiles feel different from image tiles. The client is not just placing images on a grid. It is rendering the map.

That gives you control, but it also means you need to care about tile size, layer count, geometry complexity, label density, and client performance.

Vector tile formats you will see

MVT is the format most developers mean when they say vector tiles.

FormatWhat it isUse it whenNotes
MVT / Mapbox Vector TileBinary protobuf vector tile formatYou need production web or mobile mapsDe facto standard. Used by MapLibre GL and Mapbox GL.
GeoJSON tilesGeoJSON split into tile-shaped responsesYou need debugging or small simple datasetsHuman readable but larger than MVT. Not ideal for production basemaps.
TopoJSONTopology preserving vector formatShared boundaries matter and the client supports itLess common for standard web map tile pipelines.
FlatGeobufBinary geospatial format optimized for streamingCloud native feature access, not classic slippy-map tilesUseful, but not a direct replacement for MVT basemaps.
PMTilesSingle archive containing many tilesYou want serverless or CDN-friendly tile distributionCan store vector tiles and serve them by byte range requests.

For most applications, choose MVT unless you have a specific reason not to.

Use .mvt or .pbf extensions. Both are common. The important part is the content type and renderer support.

The vector tile ecosystem

A complete vector tile stack has four parts:

  1. Data source
  2. Tile generation
  3. Tile serving
  4. Client rendering

Data sources

Common sources include:

  • OpenStreetMap for roads, places, landuse, POIs, buildings, and boundaries.

  • Natural Earth for low zoom country and physical geography layers.

  • PostGIS for application data and dynamic spatial queries.

  • Shapefiles, GeoJSON, GeoPackage, or FlatGeobuf for one-off datasets.

  • Internal operational data, such as delivery zones, assets, sensors, service areas, or risk polygons.

Tile generators

Use these when you need to convert source data into vector tiles:

  • Tippecanoe: excellent for turning GeoJSON into MBTiles.

  • Planetiler: good for large OpenStreetMap-scale pipelines.

  • PostGIS ST_AsMVT: best for dynamic tiles from database queries.

  • GDAL/OGR: useful for format conversion and preprocessing.

  • Tegola: server and generation pipeline for MVT from spatial databases.

Tile servers

Use these to expose generated tiles over HTTP:

  • Martin: fast Rust tile server for MBTiles and PostGIS.

  • TileServer GL: serves vector tiles, styles, sprites, glyphs, and rasterized maps.

  • pg_tileserv: serves PostGIS tables and functions as vector tiles.

  • Tegola: serves vector tiles from configured spatial sources.

  • Static hosting: S3, Cloudflare R2, GCS, or a CDN when tiles are pre-generated.

  • PMTiles: one archive file served over HTTP range requests.

Client renderers

For web maps:

  • MapLibre GL JS: open source and widely used.

  • Mapbox GL JS: commercial Mapbox ecosystem.

  • OpenLayers: strong GIS feature set, supports vector tile layers.

  • Deck.gl: excellent for advanced visualization and WebGL overlays.

  • Leaflet: possible with plugins, but not the natural choice for rich vector tile rendering.

If you are starting fresh and want an open source browser renderer, start with MapLibre GL JS.

How to generate vector tiles from GeoJSON with Tippecanoe

Tippecanoe is the fastest way to turn a GeoJSON file into vector tiles for testing or production.

Install it on macOS:

brew install tippecanoe

On Linux, install from your package manager if available, or build from source.

Assume you have a file called stores.geojson:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "Indiranagar Store",
        "status": "open",
        "revenue_band": "high"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [77.6412, 12.9719]
      }
    }
  ]
}

Generate an MBTiles archive:

tippecanoe \
  -o stores.mbtiles \
  -l stores \
  -zg \
  --drop-densest-as-needed \
  --extend-zooms-if-still-dropping \
  stores.geojson

What those flags mean:

  • -o stores.mbtiles: write output to an MBTiles file.
  • -l stores: name the vector tile layer stores.
  • -zg: choose a reasonable max zoom automatically.
  • --drop-densest-as-needed: if a tile gets too large, drop features in dense areas first.
  • --extend-zooms-if-still-dropping: preserve more detail by adding higher zooms if needed.

For production, do not blindly use -zg. Set zooms deliberately:

tippecanoe \
  -o stores.mbtiles \
  -l stores \
  --minimum-zoom=4 \
  --maximum-zoom=14 \
  --drop-densest-as-needed \
  --maximum-tile-bytes=500000 \
  stores.geojson

Use a max tile size budget. A common starting target is under 500 KB per tile before transport compression. For a highly interactive web app, smaller is better. A basemap with many layers may need more room, but custom overlays should usually be much smaller.

Inspect the MBTiles metadata:

sqlite3 stores.mbtiles "select name, value from metadata;"

Check whether the layer exists:

sqlite3 stores.mbtiles "select zoom_level, count(*) from tiles group by zoom_level order by zoom_level;"

If you see a huge number of tiles at high zoom and the map still feels slow, your input data may need preprocessing, simplification, or attribute trimming.

How to generate vector tiles from PostGIS

Tippecanoe is great for static files. PostGIS is better when your source data changes often or when tile requests need database filtering.

PostGIS can generate MVT using ST_AsMVT and ST_AsMVTGeom.

Here is a basic pattern:

WITH bounds AS (
  SELECT ST_TileEnvelope($1, $2, $3) AS geom
), mvtgeom AS (
  SELECT
    id,
    name,
    road_class,
    ST_AsMVTGeom(
      ST_Transform(roads.geom, 3857),
      bounds.geom,
      4096,
      64,
      true
    ) AS geom
  FROM roads, bounds
  WHERE roads.geom && ST_Transform(bounds.geom, 4326)
)
SELECT ST_AsMVT(mvtgeom, 'roads', 4096, 'geom')
FROM mvtgeom;

Parameters:

  • $1: z
  • $2: x
  • $3: y

Important details:

  • ST_TileEnvelope(z, x, y) returns the Web Mercator tile bounds.
  • ST_AsMVTGeom clips and transforms geometry into tile-local coordinates.
  • The layer name here is roads.
  • 4096 is the tile extent.
  • The buffer 64 helps avoid lines and polygons getting clipped too aggressively at tile edges.

Create a spatial index before serving tiles:

CREATE INDEX roads_geom_gix ON roads USING GIST (geom);
ANALYZE roads;

For dynamic tiles, index quality matters more than almost anything else. If your tile endpoint has to scan the whole table for every pan or zoom, it will collapse under real traffic.

Also filter attributes. Do not include everything from your table.

Bad:

SELECT *, ST_AsMVTGeom(...) AS geom FROM parcels;

Better:

SELECT
  parcel_id,
  zoning_code,
  landuse,
  ST_AsMVTGeom(...) AS geom
FROM parcels;

Every property is repeated across features. Large strings, unused metadata, and verbose JSON columns can inflate tile size quickly.

Static vs Dynamic vector tiles

There are two common serving models.

Static tiles

Static tiles is the type of tiles that, you generate tiles ahead of time and store them as files, MBTiles, or PMTiles.

Use static tiles when:

  • The data changes daily, weekly, or rarely.
  • You need predictable performance.
  • You want CDN caching.
  • You can tolerate a generation pipeline.
  • Your tile URLs do not depend on per-user filters.

A static tile URL might look like:

https://cdn.example.com/tiles/basemap/{z}/{x}/{y}.mvt

Static serving scales extremely well. A CDN can serve the same tile to many users without hitting your origin server.

Dynamic tiles

Dynamic tiles is the type of tiles that, the server generates the it when requested, usually from PostGIS.

Use dynamic tiles when:

  • Data changes frequently.
  • Tiles depend on user permissions.
  • Tiles depend on filters, time ranges, or business rules.
  • You need live operational data.

A dynamic endpoint might look like:

https://api.example.com/tiles/vehicles/{z}/{x}/{y}.mvt?status=delayed

Dynamic tiles are powerful, but they need careful caching, query tuning, and rate limits. Even a small map interaction can trigger dozens of tile requests.

Serving vector tiles correctly

A vector tile endpoint needs to get several boring details right. Most tile bugs come from these details.

MIME type

Use one of these content types:

application/vnd.mapbox-vector-tile
application/x-protobuf

application/vnd.mapbox-vector-tile is the more explicit MVT type. Some existing servers use application/x-protobuf, and renderers usually handle it.

For Nginx:

types {
    application/vnd.mapbox-vector-tile mvt;
    application/x-protobuf pbf;
}

Compression

MVT is already compact, but gzip or Brotli can still help. Many MBTiles files store tile blobs compressed. If the tile is already gzip compressed, do not gzip it again.

If you serve pre-compressed tile data, set:

Content-Encoding: gzip

Only set that header when the response body is actually gzip compressed.

CORS

If your map runs on a different domain than your tile API, set CORS headers:

Access-Control-Allow-Origin: *

Or restrict it to your app domain:

Access-Control-Allow-Origin: https://app.example.com

If tiles load in curl but not in the browser, check CORS first.

Cache-Control

For static basemap tiles:

Cache-Control: public, max-age=31536000, immutable

For dynamic tiles that change hourly:

Cache-Control: public, max-age=3600

For user-specific or permissioned tiles:

Cache-Control: private, max-age=60

The right cache header depends on the data. Do not give one-year caching to tiles that change every five minutes.

Empty tiles

A valid tile coordinate may have no data. You can return:

204 No Content

or a valid empty MVT.

For sparse datasets, 204 can save bandwidth. Just make sure your client and CDN handle it correctly.

TileJSON: the metadata contract

TileJSON tells the client how to consume a tile source.

A simple TileJSON response looks like this:

{
  "tilejson": "3.0.0",
  "name": "Stores",
  "version": "1.0.0",
  "scheme": "xyz",
  "tiles": [
    "https://tiles.example.com/stores/{z}/{x}/{y}.mvt"
  ],
  "minzoom": 4,
  "maxzoom": 14,
  "bounds": [68.1, 6.5, 97.4, 35.7],
  "center": [77.6, 12.9, 10],
  "vector_layers": [
    {
      "id": "stores",
      "description": "Retail store locations",
      "fields": {
        "name": "String",
        "status": "String",
        "revenue_band": "String"
      }
    }
  ]
}

TileJSON prevents guesswork. It tells the renderer what URL template to use, which zooms are valid, what the geographic bounds are, and which layers exist inside the tiles.

If you operate a vector tile API, expose TileJSON. Developers should not have to inspect binary tiles to discover layer names.

Styling vector tiles

Vector tile styling usually uses the Mapbox Style Specification, which MapLibre GL JS also supports.

A style has:

  • sources: where data comes from.
  • layers: how to draw data.
  • paint: visual properties like color, width, opacity.
  • layout: placement and visibility rules.

Here is a minimal style with one vector tile source and one road layer:

const style = {
  version: 8,
  sources: {
    basemap: {
      type: 'vector',
      tiles: ['https://tiles.example.com/{z}/{x}/{y}.mvt'],
      minzoom: 0,
      maxzoom: 14
    }
  },
  layers: [
    {
      id: 'roads-primary',
      type: 'line',
      source: 'basemap',
      'source-layer': 'road',
      filter: ['==', ['get', 'class'], 'primary'],
      paint: {
        'line-color': '#f97316',
        'line-width': [
          'interpolate', ['linear'], ['zoom'],
          6, 0.5,
          10, 1.5,
          14, 5
        ]
      }
    }
  ]
};

The critical property is source-layer. It must match the layer name inside the MVT tile. If your tile has a layer called roads but your style says road, nothing will render.

Common layer types

Use these style layer types often:

  • fill: polygons such as landuse, water, parks, zones.
  • line: roads, rivers, boundaries, routes.
  • circle: point datasets such as stores, incidents, vehicles.
  • symbol: labels and icons.
  • heatmap: density visualization for points.
  • fill-extrusion: 3D buildings or extruded polygons.

Data-driven styling

Vector tiles become powerful when style depends on feature attributes.

Color delivery zones by status:

paint: {
  'fill-color': [
    'match',
    ['get', 'sla_status'],
    'on_time', '#16a34a',
    'at_risk', '#f59e0b',
    'late', '#dc2626',
    '#9ca3af'
  ],
  'fill-opacity': 0.35
}

Change road width by zoom:

paint: {
  'line-width': [
    'interpolate', ['linear'], ['zoom'],
    8, 0.5,
    12, 2,
    16, 8
  ]
}

Show buildings only at higher zooms:

layout: {
  visibility: 'visible'
},
minzoom: 13

Rendering vector tiles with MapLibre GL JS

Here is a full working HTML example.

Replace the tile URL with your provider or your own endpoint. If you are using Farun or any standard vector tile API, the integration pattern is the same: add a vector source, point it at the {z}/{x}/{y} tile URL, then reference the correct source-layer names in your style layers.

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Vector tile map</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
  <style>
    body { margin: 0; }
    #map { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
  <script>
    const map = new maplibregl.Map({
      container: 'map',
      center: [77.5946, 12.9716],
      zoom: 11,
      style: {
        version: 8,
        sources: {
          basemap: {
            type: 'vector',
            tiles: [
              'https://tiles.example.com/{z}/{x}/{y}.mvt'
            ],
            minzoom: 0,
            maxzoom: 14
          }
        },
        layers: [
          {
            id: 'background',
            type: 'background',
            paint: { 'background-color': '#f8fafc' }
          },
          {
            id: 'water',
            type: 'fill',
            source: 'basemap',
            'source-layer': 'water',
            paint: { 'fill-color': '#bfdbfe' }
          },
          {
            id: 'roads',
            type: 'line',
            source: 'basemap',
            'source-layer': 'road',
            paint: {
              'line-color': '#64748b',
              'line-width': [
                'interpolate', ['linear'], ['zoom'],
                8, 0.4,
                12, 1.2,
                16, 5
              ]
            }
          }
        ]
      }
    });

    map.addControl(new maplibregl.NavigationControl());
  </script>
</body>
</html>

If your provider requires an API key, add it the way the provider supports: query string, request header through a proxy, signed URL, or tokenized tile URL. For browser maps, avoid exposing private server-side secrets. Public map tokens are normal; private API keys should go through your backend.

Adding click and hover interactivity

Vector tiles let you query features that are rendered on the map.

Example: show road attributes on click.

map.on('click', 'roads', (event) => {
  const feature = event.features[0];

  new maplibregl.Popup()
    .setLngLat(event.lngLat)
    .setHTML(`
      <strong>${feature.properties.name || 'Unnamed road'}</strong><br>
      Class: ${feature.properties.class || 'unknown'}
    `)
    .addTo(map);
});

map.on('mouseenter', 'roads', () => {
  map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'roads', () => {
  map.getCanvas().style.cursor = '';
});

This only works if the features and properties are present in the tile. If you filtered out name during tile generation, the client cannot query it later.

That is the tradeoff: fewer attributes make smaller tiles, but fewer attributes also mean less interactivity.

3D buildings with vector tiles

If your building layer has a height property, you can render simple 3D buildings.

map.addLayer({
  id: 'buildings-3d',
  type: 'fill-extrusion',
  source: 'basemap',
  'source-layer': 'building',
  minzoom: 13,
  paint: {
    'fill-extrusion-color': '#d1d5db',
    'fill-extrusion-height': [
      'coalesce',
      ['get', 'height'],
      8
    ],
    'fill-extrusion-base': 0,
    'fill-extrusion-opacity': 0.7
  }
});

Do not render all buildings at low zoom. Building polygons are heavy. Keep them for neighborhood and street-level zooms.

Backend patterns for vector tile APIs

If you build your own tile API, keep the endpoint simple:

GET /tiles/{layer}/{z}/{x}/{y}.mvt

or:

GET /tiles/{source}/{z}/{x}/{y}

Validate every coordinate:

function validateTile(z, x, y) {
  if (!Number.isInteger(z) || z < 0 || z > 22) return false;
  const max = 2 ** z;
  if (!Number.isInteger(x) || x < 0 || x >= max) return false;
  if (!Number.isInteger(y) || y < 0 || y >= max) return false;
  return true;
}

Return clear statuses:

  • 200: tile exists and body contains MVT bytes.
  • 204: valid tile coordinate but no data.
  • 400: invalid z/x/y.
  • 404: source or layer not found.
  • 500: unexpected server error.

For Node.js, a basic Express shape looks like this:

app.get('/tiles/:source/:z/:x/:y.mvt', async (req, res) => {
  const source = req.params.source;
  const z = Number(req.params.z);
  const x = Number(req.params.x);
  const y = Number(req.params.y);

  if (!validateTile(z, x, y)) {
    return res.status(400).json({ error: 'Invalid tile coordinate' });
  }

  const tile = await getTileFromMbtilesOrPostgis(source, z, x, y);

  if (!tile) {
    return res.status(204).end();
  }

  res.setHeader('Content-Type', 'application/vnd.mapbox-vector-tile');
  res.setHeader('Cache-Control', 'public, max-age=86400');
  res.send(tile);
});

If tiles are user-specific, do not use public cache headers. Add authentication, permission checks, and private caching.

Performance optimization checklist

Vector tile performance is mostly about sending less data without making the map look wrong.

1. Set a tile size budget

Start with this budget:

Custom overlay tiles: under 100 KB if possible
Normal basemap tiles: under 500 KB if possible
Heavy city tiles: investigate anything above 1 MB

Large tiles cause slow network downloads, slower protobuf decoding, higher memory use, and slower rendering.

2. Drop attributes you do not render or query

If the client does not style by a property and does not show it in a popup, remove it.

Keep:

id, class, name, status, height, category

Avoid unless needed:

long descriptions, raw source JSON, audit fields, timestamps for every feature, internal notes

3. Simplify geometry by zoom

Low zoom tiles need generalized geometry. A country boundary at zoom 4 does not need every vertex from the high resolution source.

Common approach:

  • Low zoom: simplified boundaries, major roads, major labels.
  • Mid zoom: more roads, landuse detail, important POIs.
  • High zoom: buildings, parcels, minor roads, dense labels.

4. Use feature dropping for dense point layers

Dense points can destroy map performance. Cluster, aggregate, or drop features at low zoom.

For Tippecanoe, start with:

--drop-densest-as-needed
--cluster-distance=50

For operational dashboards, consider serving aggregated vector tiles at low zoom and raw points only at high zoom.

5. Use overzooming carefully

Overzooming means using a lower zoom tile at a higher display zoom.

Example: serve z14 tiles for z15 to z18.

This reduces tile generation and storage, but too much overzooming makes geometry look simplified and labels sparse. It works well for some overlays and poorly for detailed basemaps.

6. Cache at the edge

For static or semi-static tiles, put a CDN in front. Tile traffic has strong cache locality: many users in the same city request the same tiles.

Cache keys should include any style-independent data parameters. If your endpoint supports filters like ?status=open, that query parameter must be part of the cache key.

7. Avoid too many style layers

A tile can be small and still render slowly if the style is too complex. Every style layer can trigger additional layout or paint work.

Combine layers when possible. Use filters carefully. Do not create 40 separate road layers if 8 will do.

Debugging vector tiles

When vector tiles fail, the map usually goes blank without a useful error. Debug systematically.

Problem: tiles are not requested

Check:

  • Is the source added to the style?
  • Is the layer referencing the correct source ID?
  • Are minzoom and maxzoom blocking the current zoom?
  • Is the map centered inside the source bounds?

Problem: requests happen but return errors

Open the browser Network tab and inspect tile requests.

Check:

  • 404: wrong URL template or source name.
  • 400: invalid z/x/y parsing on backend.
  • 401 or 403: missing or invalid API key.
  • CORS error: server needs Access-Control-Allow-Origin.
  • 500: backend tile generation failed.

Problem: tile requests return 200 but nothing renders

Check:

  • Is source-layer spelled exactly right?
  • Does the tile actually contain that layer?
  • Is the style filter excluding all features?
  • Is the paint opacity set to 0?
  • Is the layer underneath an opaque fill layer?
  • Are you using the wrong geometry type for the style layer?

Examples:

  • A polygon layer needs type: 'fill', not line, unless you only want outlines.
  • A line layer needs type: 'line'.
  • Points need circle or symbol.

Problem: features disappear at tile edges

This is often a clipping buffer issue. Increase the buffer during tile generation.

For PostGIS ST_AsMVTGeom, use a buffer like 64:

ST_AsMVTGeom(geom, bounds.geom, 4096, 64, true)

For Tippecanoe, inspect whether simplification or dropping is too aggressive.

Problem: labels are missing

Labels need more than points. They need a symbol layer, text field, glyph configuration, and enough attributes in the tile.

Check:

  • Does the feature have a name property?
  • Does your style use text-field: ['get', 'name']?
  • Are glyphs configured if using a full style JSON?
  • Is the symbol layer visible at the current zoom?
  • Are labels being hidden by collision detection?

Problem: map is slow

Measure tile size first. Then inspect style complexity.

Usual fixes:

  • Remove unused attributes.
  • Simplify geometry.
  • Hide dense layers until higher zooms.
  • Cluster or aggregate point data.
  • Split operational overlays from basemap tiles.
  • Cache tiles in CDN.
  • Avoid dynamic database tiles for data that can be pre-generated.

Real-world use cases

Vector tiles are useful anywhere map data needs to be interactive, styleable, and scalable.

Use vector tiles for roads, restrictions, labels, landuse, and route context. Render the calculated route as a GeoJSON or vector tile overlay. Use feature properties for road class, tunnels, bridges, ferry routes, and access restrictions.

Logistics and Fleet Tracking

Use a muted basemap and emphasize roads, depots, service areas, delivery zones, and live vehicles. At low zoom, aggregate vehicles. At high zoom, show individual assets.

Real estate maps

Use vector tiles for parcels, neighborhoods, buildings, transit, parks, school zones, and listings. Let users filter by property attributes without downloading the full dataset.

Government and civic mapping

Election boundaries, census areas, public works projects, zoning districts, and environmental layers all fit vector tiles well. They are large polygon datasets that users need to inspect at different zoom levels.

Environmental monitoring

Flood zones, deforestation areas, protected land, wildfire boundaries, and sensor networks can be tiled and styled by severity, time, or category.

Telecom and coverage maps

Coverage polygons and signal strength grids can be heavy. Vector tiles let the map load only the visible area and style coverage by band, provider, or quality.

Advanced patterns

Server-side filtering

If users filter by status, category, customer, or time range, you can push those filters into tile generation:

/tiles/incidents/{z}/{x}/{y}.mvt?severity=high&since=2026-01-01

Be careful: every unique query string can reduce cache efficiency. For high traffic apps, predefine common filters instead of allowing unlimited combinations.

Offline maps

For mobile or field apps, package vector tiles in MBTiles or PMTiles. The app can render maps without network access. This is useful for marine, logistics, emergency response, mining, agriculture, and remote field work.

Keep offline packages bounded by area and zoom range. A city at z0-z14 is manageable. A whole country at z0-z16 with buildings can be huge.

Real-time data

Do not regenerate a whole basemap for live vehicle positions. Use vector tiles for stable context and a separate real-time channel for moving objects.

Common architecture:

Basemap: vector tiles
Operational zones: vector tiles refreshed every few minutes
Live vehicles: WebSocket or polling GeoJSON
Selected route: GeoJSON overlay

Custom projections

Most web maps use EPSG:3857 Web Mercator. If you need another projection, confirm renderer support early. Standard MapLibre and Mapbox GL workflows expect Web Mercator tiles.

For specialized GIS portals, OpenLayers may be a better fit when projection flexibility matters more than WebGL basemap styling.

Developer checklist before launch

Use this before shipping a vector tile integration.

Tile source:

  • Tile URL uses {z}/{x}/{y} correctly.

  • Tile scheme is XYZ unless documented otherwise.

  • Min and max zoom are known.

  • TileJSON exists or layer names are documented.

  • Empty tiles return predictable responses.

HTTP:

  • Correct MIME type is set.

  • Compression headers match the actual body.

  • CORS works from the production app domain.

  • Cache-Control matches data freshness.

  • CDN cache key includes relevant query parameters.

Style:

  • Every style layer uses the correct source-layer.

  • Filters match actual feature properties.

  • Dense layers are hidden at low zoom.

  • Labels have glyphs and text fields configured.

  • Layer order is intentional.

Performance:

  • Heavy tiles are inspected and reduced.

  • Unused attributes are removed.

  • Geometry is simplified by zoom.

  • Point layers are clustered or aggregated where needed.

  • Dynamic database queries use spatial indexes.

Security and operations:

  • Private keys are not exposed in browser code.

  • Rate limits are understood.

  • Backend validates z/x/y values.

  • Logs capture tile errors without logging excessive request noise.

  • Monitoring tracks tile latency, error rate, and cache hit ratio.

Conclusion

Vector tiles are the foundation of modern interactive mapping. They split map data into small zoom-aware packets, send only the visible area to the client, and let the renderer draw the final map from a style specification.

The practical benefits are clear: sharper maps, lower duplication across styles, client-side interactivity, better control over visual design, and scalable delivery for large geospatial datasets.

For developers, the important skills are not theoretical. You need to know how {z}/{x}/{y} works, what source layers are inside a tile, how to style those layers, how to keep tile sizes under control, how to serve MVT with the right headers, and how to debug blank maps when one property name is wrong.

If you are using a provider such as Farun, Mapbox, MapTiler, or another vector tile API, the integration model is the same: add the provider as a vector source, reference the documented source layers, style the map for your use case, and test the tile behavior in the browser before launch.

If you are generating your own tiles, start with Tippecanoe for static datasets and PostGIS ST_AsMVT for dynamic database-backed tiles. Keep the tiles small, cache what you can, and expose TileJSON so the client has a clear contract.

Once you understand that vector tiles are data rather than images, the rest of the map stack becomes easier to reason about. The tile server delivers geometry. The style controls presentation. The renderer turns both into the map your users see.