This guide covers everything you need to know about serving vector tiles with tileserver-rs, including the differences between PMTiles and MBTiles, how to obtain tile data, and configuration options.
tileserver-rs supports two vector tile container formats:
| Format | Best For | File Extension |
|---|---|---|
| PMTiles | Production, CDN, cloud storage | .pmtiles |
| MBTiles | Development, local use, SQLite tools | .mbtiles |
PMTiles is a cloud-optimized format designed for efficient HTTP range requests. Benefits:
[[sources]]
id = "world"
type = "pmtiles"
path = "/data/world.pmtiles"
# Or serve from HTTP (requires --features http)
# path = "https://example.com/tiles/world.pmtiles"
MBTiles is an SQLite-based format. Benefits:
[[sources]]
id = "local"
type = "mbtiles"
path = "/data/local.mbtiles"
Protomaps provides free OpenStreetMap extracts in PMTiles format:
# Download a city extract (small, good for testing)
curl -L -o florence.pmtiles \
"https://build.protomaps.com/20231211/florence.pmtiles"
# Download a country extract
curl -L -o switzerland.pmtiles \
"https://build.protomaps.com/20231211/switzerland.pmtiles"
OpenMapTiles provides pre-built MBTiles files:
# Download from OpenMapTiles (requires account for larger areas)
# Small extracts are available for free
Planetiler is the fastest way to generate tiles from OSM data:
# Download and run Planetiler
wget https://github.com/onthegomap/planetiler/releases/latest/download/planetiler.jar
# Generate PMTiles for a region
java -Xmx8g -jar planetiler.jar \
--download --area=switzerland \
--output=switzerland.pmtiles
tilemaker is a lightweight alternative:
# Install tilemaker
brew install tilemaker # macOS
# Download OSM data
curl -O https://download.geofabrik.de/europe/switzerland-latest.osm.pbf
# Generate MBTiles
tilemaker --input switzerland-latest.osm.pbf --output switzerland.mbtiles
Use the pmtiles CLI to convert between formats:
# Install pmtiles CLI
npm install -g pmtiles
# or
go install github.com/protomaps/go-pmtiles/cmd/pmtiles@latest
# Convert MBTiles to PMTiles
pmtiles convert input.mbtiles output.pmtiles
# Convert PMTiles to MBTiles
pmtiles convert input.pmtiles output.mbtiles
# config.toml
[[sources]]
id = "osm"
type = "pmtiles"
path = "/data/osm.pmtiles"
name = "OpenStreetMap"
attribution = "© OpenStreetMap contributors"
Serve multiple tile sources from one server:
[[sources]]
id = "basemap"
type = "pmtiles"
path = "/data/basemap.pmtiles"
name = "Base Map"
[[sources]]
id = "terrain"
type = "mbtiles"
path = "/data/terrain.mbtiles"
name = "Terrain"
[[sources]]
id = "buildings"
type = "pmtiles"
path = "/data/buildings.pmtiles"
name = "3D Buildings"
Each source is accessible at its own endpoint:
/data/basemap/{z}/{x}/{y}.pbf/data/terrain/{z}/{x}/{y}.pbf/data/buildings/{z}/{x}/{y}.pbfServe PMTiles directly from HTTP/HTTPS URLs:
[[sources]]
id = "remote"
type = "pmtiles"
path = "https://example.com/tiles/world.pmtiles"
name = "Remote Tiles"
tileserver-rs automatically generates TileJSON 3.0 metadata for each source:
# Get metadata for a source
curl http://localhost:8080/data/osm.json
{
"tilejson": "3.0.0",
"name": "OpenStreetMap",
"attribution": "© OpenStreetMap contributors",
"tiles": ["http://localhost:8080/data/osm/{z}/{x}/{y}.pbf"],
"minzoom": 0,
"maxzoom": 14,
"bounds": [-180, -85.0511, 180, 85.0511],
"center": [0, 0, 2],
"vector_layers": [
{
"id": "water",
"fields": { "class": "string" }
},
{
"id": "transportation",
"fields": { "class": "string", "name": "string" }
}
]
}
GET /data/{source}/{z}/{x}/{y}.pbf
# Fetch a tile
curl http://localhost:8080/data/osm/10/544/373.pbf -o tile.pbf
# Check response headers
curl -I http://localhost:8080/data/osm/10/544/373.pbf
# Content-Type: application/x-protobuf
# Content-Encoding: gzip
# Cache-Control: public, max-age=86400
| Code | Meaning |
|---|---|
200 | Tile found and returned |
204 | Tile exists but is empty (no data in this area) |
404 | Source not found or coordinates outside bounds |
Convert a tile to GeoJSON for inspection:
curl http://localhost:8080/data/osm/14/8704/5972.geojson | jq .
PMTiles has better read performance than MBTiles for most use cases:
| Metric | PMTiles | MBTiles |
|---|---|---|
| Random access | ~1,300 req/s | ~750 req/s |
| Sequential access | Similar | Similar |
| Cloud serving | Excellent | Poor |
Tiles are typically gzip-compressed. tileserver-rs handles this automatically:
Content-Encoding: gzipDon't serve more zoom levels than you need:
[[sources]]
id = "overview"
type = "pmtiles"
path = "/data/overview.pmtiles"
# This file only contains z0-z10
# Requests for z11+ will return 204 (no content)
Tiles are immutable - cache them aggressively:
location ~ ^/data/.*\.pbf$ {
proxy_pass http://tileserver:8080;
proxy_cache tiles;
proxy_cache_valid 200 30d;
proxy_cache_valid 204 1h;
add_header X-Cache-Status $upstream_cache_status;
}
Vector tiles are just data. To render them, you need a style. tileserver-rs can serve styles too:
[[sources]]
id = "osm"
type = "pmtiles"
path = "/data/osm.pmtiles"
[[styles]]
id = "bright"
path = "/data/styles/bright/style.json"
The style references your tile source:
{
"version": 8,
"sources": {
"osm": {
"type": "vector",
"url": "http://localhost:8080/data/osm.json"
}
},
"layers": [...]
}
id in config# Check what sources are available
curl http://localhost:8080/data.json
# Check metadata for bounds and zoom range
curl http://localhost:8080/data/osm.json | jq '{bounds, minzoom, maxzoom}'
This is normal for areas with no data. Not every tile coordinate contains features.
For very large PMTiles files (10GB+), ensure you have enough memory:
# Check file size
ls -lh world.pmtiles
# Run with more memory if needed
RUST_LOG=debug ./tileserver-rs --config config.toml