Skip to content
Go back

Tileserver-GL vs OpenMapTiles vs renderd

By SumGuy 13 min read
Tileserver-GL vs OpenMapTiles vs renderd
Contents

Six Answers to One Question

Tileserver-GL wins for almost every home lab map stack, and renderd only wins if you need the exact openstreetmap-carto look or you’re serving legacy raster-only clients.

You want a slippy map on your self-hosted app. Not Google Maps. Not Mapbox. Something you actually control. You go looking and within fifteen minutes you’ve got six browser tabs open: OpenMapTiles, Tileserver-GL, renderd, mod_tile, MapTiler, Tegola. Everybody on Reddit seems to use a different one and nobody agrees on why.

Those six tools are not actually alternatives to each other. They sit at different layers. Some of them generate tile data, some of them serve tile data, and some of them do both in specific ways. You end up comparing them because they all touch maps, not because they’re doing the same job.

This article breaks down the three most commonly self-hosted options, OpenMapTiles, Tileserver-GL, and renderd, explains where each one fits in the stack, and gives you enough real numbers on storage and CPU to make a decision without a marketing brochure.

Which Tile Stack Should You Actually Run?

Vector vs Raster: The Fundamental Split

Before anything else, you need to decide which family you’re in.

Raster tiles are pre-rendered PNG or JPEG images. A tile at zoom level 14 is a 256×256 pixel image of that map square, fully rendered with colors, roads, labels, everything. Your browser or app just stitches them together like a mosaic. Fast to display, no client-side work required. The downside: you can’t change the style without re-rendering. You can’t rotate the map cleanly. And pre-rendering tiles for the whole world at every zoom level produces a volume of data measured in terabytes.

Vector tiles ship raw geometry (coordinates, lines, polygons, metadata) in a compact binary format (Mapbox Vector Tile, .mvt). Rendering happens in the browser or app using a style sheet (GL JS, MapLibre, Mapbox GL). The same tile data renders in dark mode, in a pastel style, in satellite-hybrid, whatever the style sheet specifies. Tiles are typically 10 to 100x smaller than equivalent raster tiles. The client does more work, but modern phones and browsers handle it easily.

In 2026 you almost certainly want vector tiles unless you have a specific reason not to. The exceptions are real, we’ll get to them, but the default should be vector.

OpenMapTiles: The Data Pipeline, Not the Server

This is the most common confusion. OpenMapTiles is not a tile server. It’s a tile schema and a data pipeline that produces a tile database you then serve with something else.

The OpenMapTiles schema defines which OSM features go into which tile layers (landuse, waterway, building, poi, transportation, etc.) and at which zoom levels they appear. It’s an opinionated subset of OSM data, normalized for cartographic use. Most Mapbox GL compatible map styles you’ll find online expect tiles in the OpenMapTiles schema.

The pipeline that produces those tiles has evolved. The original approach used osm2pgsql to load raw OSM data into PostGIS, then ran a tile generator (tippecanoe, tilemaker) against PostGIS to produce an MBTiles file. That works but it’s slow and resource-heavy.

The modern approach is planetiler, a Java-based tool from Google that reads the OSM PBF directly, skipping the PostGIS step entirely, and produces an MBTiles or PMTiles file in a single pass. For a North America extract it runs in about 30 to 45 minutes on reasonable hardware. The full planet takes 2 to 4 hours. The output is a complete vector tile database compatible with the OpenMapTiles schema.

Terminal window
# Planetiler one-liner: US extract → MBTiles, ~30 min
docker run --rm -it -v "$(pwd)/data":/data \
ghcr.io/onthegomap/planetiler:latest \
--download --area=us \
--output=/data/us.mbtiles

What you get is a single file. No running database. No PostGIS. Just us.mbtiles. Then you point a tile server at it.

Tileserver-GL: The Vector Tile Server That Does Raster On Demand

Tileserver-GL is a Node.js tile server that serves vector tiles directly from MBTiles or PMTiles files and, optionally, renders raster tiles on demand using MapLibre GL Native (the server-side renderer).

The vector tile path is lightweight. It reads a tile from the file, returns the raw .mvt binary. No rendering, no style processing, no GPU. A Raspberry Pi 4 can serve hundreds of vector tile requests per second this way. RAM usage is low. CPU usage is low. The tile file is memory-mapped so hot tiles live in the OS page cache.

The on-demand raster path is where things get expensive. When a client requests a PNG tile, Tileserver-GL renders it using a GL style, the same pipeline MapLibre runs in the browser, now on the server. This is CPU-intensive and single-threaded per render. You can run multiple workers, but each render still blocks a thread. On a 4-core machine you can sustain maybe 20 to 60 raster renders per second depending on zoom level and style complexity.

Here’s a minimal Compose stack for Tileserver-GL serving your planetiler output:

docker-compose.yml
services:
tileserver:
image: maptiler/tileserver-gl:latest
container_name: tileserver-gl
ports:
- "8080:8080"
volumes:
- ./data:/data
command: --config /data/config.json
restart: unless-stopped
data/config.json
{
"options": {
"paths": {
"mbtiles": "/data"
}
},
"data": {
"us-tiles": {
"mbtiles": "us.mbtiles"
}
},
"styles": {
"osm-bright": {
"style": "osm-bright/style.json",
"tilejson": {
"bounds": [-180, -85.0511, 180, 85.0511]
}
}
}
}

Tileserver-GL also exposes a TileJSON endpoint, which is how clients discover what’s available:

{
"tilejson": "2.2.0",
"name": "us-tiles",
"format": "pbf",
"tiles": ["http://localhost:8080/data/us-tiles/{z}/{x}/{y}.pbf"],
"minzoom": 0,
"maxzoom": 14,
"bounds": [-179.9, 18.8, -66.9, 71.4]
}

Point MapLibre GL JS at that TileJSON URL and you have a working self-hosted slippy map.

renderd + mod_tile: The Classic Raster Stack

renderd is the rendering daemon that powers the original openstreetmap.org tile rendering pipeline. It pairs with mod_tile (an Apache module; nginx proxies to the renderd socket directly) to handle tile request queuing, caching, and expiry. The stack looks like this: HTTP request → mod_tile → renderd → Mapnik → PNG tile → cache → response.

Mapnik is the rendering engine. It reads from a PostGIS database (populated by osm2pgsql) and renders tiles according to an XML style sheet. The reference style is openstreetmap-carto, the look you see on osm.org.

This stack has been battle-tested for fifteen years. It works. But it’s architecturally raster-first and it requires the full PostGIS stack to be running, you can’t serve from a flat file. Here’s a representative Compose setup:

docker-compose.yml
services:
db:
image: postgis/postgis:16-3.4
container_name: osm-db
environment:
POSTGRES_DB: gis
POSTGRES_USER: gis
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
renderd:
image: overv/openstreetmap-tile-server:2.4.0
container_name: renderd
ports:
- "80:80"
environment:
DOWNLOAD_PBF: https://download.geofabrik.de/north-america/us/california-latest.osm.pbf
DOWNLOAD_POLY: https://download.geofabrik.de/north-america/us/california.poly
volumes:
- tile-cache:/var/lib/mod_tile
- osm-data:/data/database
shm_size: 256mb
depends_on:
- db
restart: unless-stopped
volumes:
pgdata:
osm-data:
tile-cache:

The import step populates PostGIS via osm2pgsql. For California this takes around 20 to 40 minutes. For the full US, expect several hours. The tile cache fills on demand: the first request to a tile renders it and caches the PNG; subsequent requests serve from cache. Pre-seeding (“warming”) the cache is slow because you’re rendering millions of tiles.

Storage Realities: Where the Gigabytes Go

This is where things get concrete.

Vector tiles (MBTiles, planetiler output):

CoverageFile Size
Single city50 to 200 MB
Single US state (California)1 to 3 GB
United States~10 GB
Full planet~50 GB

That’s the whole thing. All zoom levels 0 to 14, the full OpenMapTiles schema. A 50 GB planet file that serves vector tiles is genuinely achievable on a consumer NVMe. Planetiler gets there by discarding everything cartographically irrelevant and encoding geometry efficiently.

Raster tiles (pre-rendered PNGs):

Pre-rendering the full planet at all zoom levels is a multi-petabyte exercise nobody actually does in a home lab. The growth is exponential, each zoom level has four times the tiles of the previous one. Zoom 14 alone for the whole planet is billions of tiles.

The practical approach is render-on-demand + cache (what mod_tile does) or pre-render only for specific bounding boxes and zoom levels. For a region like a major city at zoom levels 0 to 15, you’re looking at 10 to 100 GB of cached PNGs. For a whole country, it’s terabytes if you pre-seed deep zoom levels.

PostGIS database (for renderd):

Running the full US in PostGIS for osm2pgsql occupies roughly 200 to 400 GB depending on import style. Running the full planet: 1 to 2 TB. This is not a storage format, it’s an operational database that needs to stay running and get updated.

CPU and RAM: The Serving Patterns

Tileserver-GL, vector mode:

Tileserver-GL, on-demand raster mode:

renderd:

The pattern: vector serving is I/O-bound and cheap. Raster rendering is CPU-bound and expensive. Pre-rendered raster serving from disk is cheap but requires enormous disk up front.

The PMTiles Dark Horse

PMTiles is worth understanding because it changes the deployment model in a useful way.

Standard MBTiles is a SQLite database. It works great on a server, but you can’t put it on a CDN or S3 and serve tiles directly without a server-side component. PMTiles solves this by using HTTP range requests against a single binary file. The file is structured so that a JavaScript client can fetch just the tile it needs using a Range header, no tile server required.

Terminal window
# Planetiler can output PMTiles directly
docker run --rm -it -v "$(pwd)/data":/data \
ghcr.io/onthegomap/planetiler:latest \
--download --area=us \
--output=/data/us.pmtiles

Upload us.pmtiles to Cloudflare R2 or any S3-compatible bucket with CORS configured, point the PMTiles JS library at it, and you have serverless tile serving. No Compose stack. No process to maintain. Just a file on object storage.

The catch: HTTP range requests have some overhead per tile. For low-to-medium traffic this is invisible. For a high-traffic public site it gets expensive (in API request costs, not compute). For a private homelab app or a low-traffic site, PMTiles on R2 is genuinely the easiest stack.

If you do want a server but want the simplicity of a single file, Tileserver-GL supports PMTiles directly in recent versions. Same deployment, same config, just a .pmtiles path instead of .mbtiles.

Pick Your Stack

Here’s the actual decision tree, without any marketing involved.

Most self-hosters in 2026 should run:

planetiler → us.mbtiles or us.pmtiles → Tileserver-GL (or PMTiles serverless)

Takes an afternoon to set up, uses reasonable disk, serves vector tiles to any MapLibre GL or Mapbox GL JS client. If your clients are modern browsers or apps, this is it.

If you want zero server maintenance:

planetiler → us.pmtiles → Cloudflare R2 + PMTiles JS

Upload once. Forget about it. Pay a few cents a month in R2 storage costs. No process to restart, no disk to fill, no server to patch.

If you need raster tiles and your clients are modern:

planetiler → MBTiles → Tileserver-GL (raster mode)

Turn on raster rendering in Tileserver-GL config, run 2 to 4 render workers, throw a Caddy reverse proxy in front for caching. You get raster tiles without the PostGIS footprint.

If you need the openstreetmap-carto style specifically, or you have legacy GIS clients that only speak raster:

osm2pgsql → PostGIS → renderd + mod_tile

This is the right call if you’re running something that expects the exact OSM house style, or if you have software from 2015 that only speaks XYZ raster tiles and you’re not rewriting it.

If you’re comparing city coverage (small bounding box) at high zoom levels for a specialized app: Consider a self-hosted Tegola or pg_tileserv against a PostGIS database. Those tools read PostGIS on the fly and generate vector tiles without a pre-generation step. More flexible, more operationally complex.

When renderd Is Actually the Right Call

People talk about renderd like it’s ancient legacy that anyone sensible has moved past. That’s not entirely fair.

If you need the openstreetmap-carto style (the actual look of osm.org), renderd + Mapnik is the only real option. There’s no OpenMapTiles-schema style that’s a faithful replica of it. The cartography is different.

If you’re building a raster tile archive (pre-seeding tiles for an area you’ll serve offline to devices without a renderer, like field GIS tablets or embedded systems), renderd’s seeding tools are mature. mod_tile’s caching layer handles tile expiry and re-render scheduling with controls that Tileserver-GL’s raster mode doesn’t have.

If you need Mapnik’s rendering features (custom fonts, complex symbolizers, pattern fills, shields), Mapnik gives you more cartographic control than MapLibre server-side rendering.

For everything else, the planetiler → Tileserver-GL pipeline gets you there faster and cheaper.

The Short Version

The confusion clears up once you separate the layers:

The storage gap is real: a 50 GB planet in PMTiles vs. terabytes of pre-rendered raster PNGs. For home lab use, that alone usually decides it.

Run planetiler on a weekend, drop the file on R2 or into Tileserver-GL, and you’ve got a self-hosted map stack that actually scales. Your 2 AM self will appreciate not having to debug a Postgres tablespace error when the tile cache runs out of space.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Synthetic Browser Monitoring with Playwright + Grafana

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts