Benchmarks Tile servers Matchup

Candidates

Martin vs go-pmtiles vs tileserver-gl vs mbtileserver vs BBOX

Tick the candidates you care about and the charts and tables redraw for just them: same run, same rig, same samples. The verdict stays on the published page.

Scorecard

Mean of each section's samples for the candidates you picked. The best value in each column is marked in red.
CandidateSingle-tile latency (ms, lower is better)Throughput under concurrency (req/s, higher is better)Random tile walk (req/s, higher is better)Viewport burst (ms, lower is better)Cold start and footprint (ms, lower is better)
Martin (PMTiles)1.2 ms12634.2 req/s12992.8 req/s1.2 ms158.4 ms
Martin (MBTiles)1.2 ms9185.5 req/s13724.0 req/s1.2 ms153.2 ms
go-pmtiles2.0 ms4513.5 req/s4533.1 req/s1.9 ms157.4 ms
tileserver-gl-light14.3 ms281.2 req/s793.2 req/s18.7 ms276.4 ms
mbtileserver1.1 ms7856.6 req/s7110.4 req/s2.2 ms137.2 ms
BBOX (PMTiles)2.3 ms3961.5 req/s8292.2 req/s1.9 ms117.6 ms
BBOX (MBTiles)2.7 ms2202.2 req/s6503.4 req/s2.6 ms149.8 ms

Single-tile latency

One client, one tile at a time: mean response time for a dense street-level tile, a district tile, a regional tile and an overview tile, each read from the archive on every request.

Test 1 Street level z14

Mean latency over one connection. The densest kind of tile: a 1.5 km Prenzlauer Berg tile with every building, road, POI and landuse polygon of the OpenMapTiles schema. (LOWER ms = FASTER)

Street level z14 (ms)
Martin (PMTiles)1.2 ms
Martin (MBTiles)1.2 ms
go-pmtiles2.0 ms
tileserver-gl-light14.3 ms
mbtileserver1.1 ms
BBOX (PMTiles)2.3 ms
BBOX (MBTiles)2.7 ms
Street level z14 in ms
Martin (PMTiles)1.2 ms
Martin (MBTiles)1.2 ms
go-pmtiles2.0 ms
tileserver-gl-light14.3 ms
mbtileserver1.1 ms
BBOX (PMTiles)2.3 ms
BBOX (MBTiles)2.7 ms

Test 2 District z12

Mean latency over one connection. Inner-city tile covering Prenzlauer Berg and Mitte: roads, landuse, water and place labels, buildings dropped. (LOWER ms = FASTER)

District z12 (ms)
Martin (PMTiles)1.1 ms
Martin (MBTiles)0.8 ms
go-pmtiles1.4 ms
tileserver-gl-light3.6 ms
mbtileserver0.9 ms
BBOX (PMTiles)1.4 ms
BBOX (MBTiles)1.4 ms
District z12 in ms
Martin (PMTiles)1.1 ms
Martin (MBTiles)0.8 ms
go-pmtiles1.4 ms
tileserver-gl-light3.6 ms
mbtileserver0.9 ms
BBOX (PMTiles)1.4 ms
BBOX (MBTiles)1.4 ms

Test 3 Region z9

Mean latency over one connection. Low-zoom tile covering most of Berlin: boundaries, major roads, water, place names. (LOWER ms = FASTER)

Region z9 (ms)
Martin (PMTiles)1.0 ms
Martin (MBTiles)0.8 ms
go-pmtiles1.0 ms
tileserver-gl-light2.7 ms
mbtileserver0.8 ms
BBOX (PMTiles)1.0 ms
BBOX (MBTiles)1.0 ms
Region z9 in ms
Martin (PMTiles)1.0 ms
Martin (MBTiles)0.8 ms
go-pmtiles1.0 ms
tileserver-gl-light2.7 ms
mbtileserver0.8 ms
BBOX (PMTiles)1.0 ms
BBOX (MBTiles)1.0 ms

Test 4 Overview z5

Mean latency over one connection. Small tile at the top of the pyramid: the extract's footprint in one tile, mostly boundaries and a few labels. (LOWER ms = FASTER)

Overview z5 (ms)
Martin (PMTiles)1.0 ms
Martin (MBTiles)0.9 ms
go-pmtiles0.8 ms
tileserver-gl-light1.6 ms
mbtileserver0.7 ms
BBOX (PMTiles)0.6 ms
BBOX (MBTiles)0.6 ms
Overview z5 in ms
Martin (PMTiles)1.0 ms
Martin (MBTiles)0.9 ms
go-pmtiles0.8 ms
tileserver-gl-light1.6 ms
mbtileserver0.7 ms
BBOX (PMTiles)0.6 ms
BBOX (MBTiles)0.6 ms

Throughput under concurrency

Closed-loop load on the street level z14 tile at 10 and 100 concurrent connections; every request reads the same tile out of the archive.

Test 1 Throughput, 10 connections

Successful street level z14 tiles per second with 10 closed-loop clients. (HIGHER req/s = BETTER)

Throughput, 10 connections (req/s)
Martin (PMTiles)11289.1 req/s
Martin (MBTiles)11145.5 req/s
go-pmtiles2471.6 req/s
tileserver-gl-light300.7 req/s
mbtileserver5908.0 req/s
BBOX (PMTiles)3884.6 req/s
BBOX (MBTiles)3509.3 req/s
Throughput, 10 connections in req/s
Martin (PMTiles)11289.1 req/s
Martin (MBTiles)11145.5 req/s
go-pmtiles2471.6 req/s
tileserver-gl-light300.7 req/s
mbtileserver5908.0 req/s
BBOX (PMTiles)3884.6 req/s
BBOX (MBTiles)3509.3 req/s

Test 2 Throughput, 100 connections

Successful street level z14 tiles per second with 100 closed-loop clients. (HIGHER req/s = BETTER)

Throughput, 100 connections (req/s)
Martin (PMTiles)12634.2 req/s
Martin (MBTiles)9185.5 req/s
go-pmtiles4513.5 req/s
tileserver-gl-light281.2 req/s
mbtileserver7856.6 req/s
BBOX (PMTiles)3961.5 req/s
BBOX (MBTiles)2202.2 req/s
Throughput, 100 connections in req/s
Martin (PMTiles)12634.2 req/s
Martin (MBTiles)9185.5 req/s
go-pmtiles4513.5 req/s
tileserver-gl-light281.2 req/s
mbtileserver7856.6 req/s
BBOX (PMTiles)3961.5 req/s
BBOX (MBTiles)2202.2 req/s

Test 3 p99 latency, 10 connections

Median across passes of each pass's p99 response time at 10 connections. A server that completed no request in a pass is charged the whole 12 s pass. (LOWER ms = FASTER)

p99 latency, 10 connections (ms)
Martin (PMTiles)1.7 ms
Martin (MBTiles)1.6 ms
go-pmtiles14.8 ms
tileserver-gl-light48.9 ms
mbtileserver7.1 ms
BBOX (PMTiles)4.1 ms
BBOX (MBTiles)5.5 ms
p99 latency, 10 connections in ms
Martin (PMTiles)1.7 ms
Martin (MBTiles)1.6 ms
go-pmtiles14.8 ms
tileserver-gl-light48.9 ms
mbtileserver7.1 ms
BBOX (PMTiles)4.1 ms
BBOX (MBTiles)5.5 ms

Test 4 p99 latency, 100 connections

Median across passes of each pass's p99 response time at 100 connections. A server that completed no request in a pass is charged the whole 12 s pass. (LOWER ms = FASTER)

p99 latency, 100 connections (ms)
Martin (PMTiles)203.1 ms
Martin (MBTiles)13.8 ms
go-pmtiles108.8 ms
tileserver-gl-light449.4 ms
mbtileserver34.3 ms
BBOX (PMTiles)31.3 ms
BBOX (MBTiles)65.0 ms
p99 latency, 100 connections in ms
Martin (PMTiles)203.1 ms
Martin (MBTiles)13.8 ms
go-pmtiles108.8 ms
tileserver-gl-light449.4 ms
mbtileserver34.3 ms
BBOX (PMTiles)31.3 ms
BBOX (MBTiles)65.0 ms

Test 5 Failed requests, 100 connections

Non-2xx responses plus connection errors and 60 s timeouts as a share of all requests at 100 connections. (LOWER % = FASTER)

Failed requests, 100 connections (%)
Martin (PMTiles)0 %
Martin (MBTiles)0 %
go-pmtiles0 %
tileserver-gl-light0 %
mbtileserver0 %
BBOX (PMTiles)0 %
BBOX (MBTiles)0 %
Failed requests, 100 connections in %
Martin (PMTiles)0 %
Martin (MBTiles)0 %
go-pmtiles0 %
tileserver-gl-light0 %
mbtileserver0 %
BBOX (PMTiles)0 %
BBOX (MBTiles)0 %

Random tile walk

10 clients each requesting a random z14 tile from a 256-tile block of central Berlin, so no server can win on one hot tile and every request goes through the archive's index.

Test 1 Throughput, 10 connections

Successful tiles per second when 10 closed-loop clients each request a random z14 tile from a 256-tile block of central Berlin. (HIGHER req/s = BETTER)

Throughput, 10 connections (req/s)
Martin (PMTiles)12992.8 req/s
Martin (MBTiles)13724.0 req/s
go-pmtiles4533.1 req/s
tileserver-gl-light793.2 req/s
mbtileserver7110.4 req/s
BBOX (PMTiles)8292.2 req/s
BBOX (MBTiles)6503.4 req/s
Throughput, 10 connections in req/s
Martin (PMTiles)12992.8 req/s
Martin (MBTiles)13724.0 req/s
go-pmtiles4533.1 req/s
tileserver-gl-light793.2 req/s
mbtileserver7110.4 req/s
BBOX (PMTiles)8292.2 req/s
BBOX (MBTiles)6503.4 req/s

Test 2 p99 latency, 10 connections

Median across passes of each pass's p99 response time on the random walk. A server that completed no request in a pass is charged the whole 12 s pass. (LOWER ms = FASTER)

p99 latency, 10 connections (ms)
Martin (PMTiles)1.5 ms
Martin (MBTiles)1.4 ms
go-pmtiles8.5 ms
tileserver-gl-light33.8 ms
mbtileserver5.8 ms
BBOX (PMTiles)2.8 ms
BBOX (MBTiles)3.5 ms
p99 latency, 10 connections in ms
Martin (PMTiles)1.5 ms
Martin (MBTiles)1.4 ms
go-pmtiles8.5 ms
tileserver-gl-light33.8 ms
mbtileserver5.8 ms
BBOX (PMTiles)2.8 ms
BBOX (MBTiles)3.5 ms

Viewport burst

Four adjacent z14 tiles requested at once, the way a map view loads; wall time until the last one lands.

Test 1 Viewport, mean

Mean wall time until all four z14 tiles have arrived, requested in parallel. (LOWER ms = FASTER)

Viewport, mean (ms)
Martin (PMTiles)1.2 ms
Martin (MBTiles)1.2 ms
go-pmtiles1.9 ms
tileserver-gl-light18.7 ms
mbtileserver2.2 ms
BBOX (PMTiles)1.9 ms
BBOX (MBTiles)2.6 ms
Viewport, mean in ms
Martin (PMTiles)1.2 ms
Martin (MBTiles)1.2 ms
go-pmtiles1.9 ms
tileserver-gl-light18.7 ms
mbtileserver2.2 ms
BBOX (PMTiles)1.9 ms
BBOX (MBTiles)2.6 ms

Test 2 Viewport, slowest

The slowest of the measured viewport loads. (LOWER ms = FASTER)

Viewport, slowest (ms)
Martin (PMTiles)1.7 ms
Martin (MBTiles)1.9 ms
go-pmtiles4.8 ms
tileserver-gl-light22.3 ms
mbtileserver11.6 ms
BBOX (PMTiles)3.2 ms
BBOX (MBTiles)5.7 ms
Viewport, slowest in ms
Martin (PMTiles)1.7 ms
Martin (MBTiles)1.9 ms
go-pmtiles4.8 ms
tileserver-gl-light22.3 ms
mbtileserver11.6 ms
BBOX (PMTiles)3.2 ms
BBOX (MBTiles)5.7 ms

Cold start and footprint

Time from process start to the first served tile, and resident memory idle and after load.

Test 1 Time to first tile

Mean time from container process start until the street-level tile is served, including opening the archive and reading its metadata, over five starts. (LOWER ms = FASTER)

Time to first tile (ms)
Martin (PMTiles)158.0 ms
Martin (MBTiles)153.0 ms
go-pmtiles157.0 ms
tileserver-gl-light276.0 ms
mbtileserver137.0 ms
BBOX (PMTiles)118.0 ms
BBOX (MBTiles)150.0 ms
Time to first tile in ms
Martin (PMTiles)158.0 ms
Martin (MBTiles)153.0 ms
go-pmtiles157.0 ms
tileserver-gl-light276.0 ms
mbtileserver137.0 ms
BBOX (PMTiles)118.0 ms
BBOX (MBTiles)150.0 ms

Test 2 Idle memory

Container RSS five seconds after the first tile, before any load. (LOWER MB = FASTER)

Idle memory (MB)
Martin (PMTiles)9.1 MB
Martin (MBTiles)9.8 MB
go-pmtiles13.1 MB
tileserver-gl-light46.2 MB
mbtileserver5.5 MB
BBOX (PMTiles)11.4 MB
BBOX (MBTiles)11.3 MB
Idle memory in MB
Martin (PMTiles)9.1 MB
Martin (MBTiles)9.8 MB
go-pmtiles13.1 MB
tileserver-gl-light46.2 MB
mbtileserver5.5 MB
BBOX (PMTiles)11.4 MB
BBOX (MBTiles)11.3 MB

Test 3 Memory after load

Container RSS right after the concurrency passes. (LOWER MB = FASTER)

Memory after load (MB)
Martin (PMTiles)167.7 MB
Martin (MBTiles)63.2 MB
go-pmtiles44 MB
tileserver-gl-light308.7 MB
mbtileserver66.2 MB
BBOX (PMTiles)34.9 MB
BBOX (MBTiles)68.9 MB
Memory after load in MB
Martin (PMTiles)167.7 MB
Martin (MBTiles)63.2 MB
go-pmtiles44 MB
tileserver-gl-light308.7 MB
mbtileserver66.2 MB
BBOX (PMTiles)34.9 MB
BBOX (MBTiles)68.9 MB
Test conditions: rig, corpus, protocol
Rig
Machine
MacBook Pro
Chip
Apple M2 Max
Cores
12 CPU cores (8 performance, 4 efficiency)
Memory
96 GB
OS
macOS 26.6.2 (arm64)
Runtime
Docker 29.4.0 (OrbStack); one tile server container at a time (4 CPUs, 4 GB) with both archives bind-mounted read-only; oha 1.16.0 on the host
Protocol
Warmups
3 unmeasured passes
Measured
20 passes per tool
Process
One server container at a time reading one of two archives with identical tiles (berlin.mbtiles from Planetiler 0.10.2, berlin.pmtiles converted from it by go-pmtiles); 4 workers where the concept exists; 5 cold starts, 20 single-tile passes of 20 requests per tile, 20 four-tile viewport bursts, 8 passes of 12 s at 10 and at 100 connections on one tile, 8 passes of 12 s at 10 connections over 256 random tiles
Cache
No server-side tile cache configured anywhere (go-pmtiles keeps its default 64 MB directory cache); archives in the page cache after warmups; every request accepts gzip, as map clients do, except the identity variant
Output
oha 1.16.0 JSON (latency percentiles, request counts, status codes); Bun fetch for cold start, parity and viewport bursts; docker stats for RSS

What this does not prove

  • One laptop: the server under test (4 CPUs, 4 GB) and the load generator shared a 12-core M2 Max under OrbStack. Numbers are relative to each other, not absolute capacity.
  • At the top of the table the harness is part of the measurement: 12,600 tiles a second of 242 KB is about 3 GB/s through the Docker port forward into oha on the same host. The gaps between Martin, mbtileserver and go-pmtiles are real in direction; their size at the top may be understated.
  • Every request sends Accept-Encoding: gzip, as browsers and MapLibre do, so a server can pass the stored gzip bytes through and nobody is charged for the client's choice. What each server does for a client that sends no Accept-Encoding is recorded but not scored: Martin and BBOX honour the missing header and decompress the tile on every request (2.9 ms and 9 to 10 ms for the street-level tile instead of 1.2 and 2.3 to 2.8 ms), while go-pmtiles, mbtileserver and tileserver-gl send Content-Encoding: gzip regardless. Both are defensible choices for a tile server; ranking them would charge the first group for following the request. A first full run that sent no Accept-Encoding on every request did exactly that and was discarded.
  • tileserver-gl is measured as tileserver-gl-light, a single Node.js process without MapLibre Native. It gunzips every stored tile and re-gzips it before sending (243,510 bytes out for a 242,407-byte stored tile), which is where its 12 ms go. Server-side raster rendering, the reason many people run it, is not measured here.
  • go-pmtiles ran with its default 64 MB directory cache; it caches PMTiles directory entries, not tiles. go-pmtiles is designed to serve archives from object storage over HTTP range requests; a local file is the simplest case for it, not its common one.
  • Martin's PMTiles source showed a p99 of about 203 ms in every one of its eight 100-client passes while its MBTiles source stayed at 14 ms, and its container held 168 MB after load against 63 MB for MBTiles. Both are reported, neither is explained by this run.
  • The archive is 41 MB and sat entirely in the page cache after the warmups. This is hot-cache serving: server overhead, archive index lookup and delivery, not disk or object-storage behaviour. A planet-scale archive of 80 GB or more, where PMTiles directory lookups and the MBTiles SQLite index stop fitting in memory, is a different benchmark.
  • Planetiler built the MBTiles with --compact_db=false because BBOX 0.6.2 rejects Planetiler's default compact schema (tiles_shallow plus tiles_data). The flat tiles table is the standard MBTiles layout; the compact one would have been smaller and may serve differently.
  • Cold start is process start to first tile, so it includes opening the archive and reading its metadata; every server did it in 118 to 276 ms on average, and the spread between a server's five starts (BBOX PMTiles 61 to 135 ms) is of the same order as the gaps between servers below tileserver-gl.
  • Memory is Docker's reported usage for the container (cgroup usage minus inactive file cache), sampled once idle and once after the random walk.
  • BBOX sends no Content-Type header on tiles from its PMTiles source (it does from MBTiles); tileserver-gl's re-compressed tiles differ in size from the stored bytes. Feature counts and layers were identical for all seven candidates on all four test tiles.
  • mbtileserver ran from the project's linux_arm64 v0.11.0 release binary, packaged on debian:bookworm-slim, because its published image is amd64-only. BBOX ran from a source build of v0.6.2 for the same reason.
  • Single run on one day. No candidate failed a single request in any pass; every sample is in the run file.

Rerun it yourself: the runner and configuration are in the repository, and the methodology page covers what every run holds constant. Think a result is wrong? Open an issue with your rig and your samples.

Read the published benchmark

Martin is a project of the MapLibre organization. go-pmtiles and the PMTiles format are developed by Protomaps. tileserver-gl is developed by MapTiler. mbtileserver is developed by the Conservation Biology Institute. BBOX is developed by Sourcepole. Planetiler is developed by Michael Barry and contributors; the tile schema is OpenMapTiles. Map data: OpenStreetMap contributors, ODbL, via the Geofabrik Berlin extract of 2026-01-01; water polygons from OSMData, Natural Earth public domain.