MBTiles — SQLite Tile Container for Offline Map Apps (Mapbox)

2026-05-0410 min read
MBTilesMapboxSQLiteoffline mapsvector tilestiles

MBTiles is a packaging scheme that puts millions of map tiles into a single ordinary SQLite file with the .mbtiles extension. Inside live just two main tables: tiles with three indexing columns (zoom_level, tile_column, tile_row) and a tile_data BLOB column, plus a key-value metadata table describing bbox, format, version, and attribution. Mapbox invented the format in January 2011 to ship vector and raster tiles as one file instead of dragging thousands of tiny .png files across a mobile filesystem. The spec is open (BSD-2-Clause), current version 1.3. In 14 years MBTiles became the standard for offline cartography: OSMAnd, Maps.me (Organic Maps), Locus Map, OruxMaps, MapTiler, GeoServer, TileServer-GL, mbtileserver — every one reads MBTiles out of the box. On osm2cdr.ru the MBTiles export assembles raster PNG tiles or vector MVT tiles (encoded by the service's own writer) into a single SQLite container ready for mobile-app import or serving through any tile server.

Map generation of Prague city center

History: Mapbox 2011, OSMAnd, Maps.me

The story of MBTiles starts at Mapbox — a startup that in 2010 was just spinning out of Development Seed. The team had a simple engineering problem: how to deliver a pre-rendered basemap for offline viewing without making the user download 50,000 separate PNG files. Obvious answer — pack it into one container. They picked SQLite because the library was already on every mobile device (Android and iOS use SQLite for system needs), it could be read without unpacking, and it provided indexed random-access instead of linear reads.

In January 2011 Mapbox published the first spec on GitHub as mbtiles-spec under BSD-2-Clause — maximally permissive, no copyleft. Versions 1.0 and 1.1 defined raster tiles only (PNG/JPEG); version 1.2 (2013) added vector tiles (Mapbox Vector Tile, born in parallel); version 1.3 (2016) formalized UTF-8 grids for interactive overlays and exact metadata rules.

Adoption was fast. By late 2011 Maps.me (then MapsWithMe) switched to MBTiles as its primary offline distribution format. OSMAnd added MBTiles as a secondary format (its native OBF remained primary, but MBTiles took the overlay and raster-basemap slots). By 2014 every mobile map app supported MBTiles. TileServer-GL (from Klokan Tech, the MapTiler authors) made MBTiles the primary input for server-side serving — one SQLite file, one XYZ endpoint.

In 2020 a rival appeared — PMTiles from Protomaps. Same big idea (single-file tile archive), but PMTiles is optimized for HTTP range requests and static cloud hosting on S3/R2, while MBTiles needs a SQLite runtime to read. That split the ecosystem: PMTiles became the standard for serverless web maps, MBTiles stayed the standard for offline mobile apps and self-hosted tile servers.

Inside MBTiles: SQLite schema

MBTiles is a plain SQLite 3 file you can open in sqlite3 file.mbtiles or DB Browser for SQLite and see its guts. The spec mandates at minimum two tables:

tiles table. Holds the tiles themselves:

CREATE TABLE tiles (
  zoom_level INTEGER,
  tile_column INTEGER,
  tile_row INTEGER,
  tile_data BLOB
);
CREATE UNIQUE INDEX tile_index ON tiles (zoom_level, tile_column, tile_row);

Each row is one z/x/y tile with binary data (PNG, JPEG, WebP for raster; protobuf MVT, usually gzip-compressed, for vector). The unique index on (z, x, y) gives O(log n) lookup.

metadata table. A key-value description:

CREATE TABLE metadata (name TEXT, value TEXT);

Required keys: name, format (png, jpg, webp, pbf), bounds (minimum WGS84 bbox lon_min,lat_min,lon_max,lat_max), minzoom, maxzoom. Optional: attribution, description, version, type (baselayer or overlay), json (for vector — describes layers and field schemas).

The key pitfall — TMS vs XYZ Y axis. Per spec MBTiles uses TMS (Tile Map Service from OSGeo): tile_row=0 is the southern (bottom) tile row. Most web mapping libraries (Leaflet, MapLibre, Mapbox GL, OpenLayers) use XYZ scheme: y=0 is the northern (top) row. So when serving MBTiles through an XYZ endpoint you must flip: y_xyz = (2^zoom - 1) - y_tms. TileServer-GL does this automatically, but if you read SQLite directly — account for it.

Map generation of Sochi city center

Use cases: offline maps, mobile apps, self-hosted

Offline mobile navigation. The historical primary use case. A user downloads a regional .mbtiles file (e.g. Czech Republic 80 MB vector or 800 MB raster) and drops it into the app directory. OSMAnd, Locus Map, OruxMaps, Maps.me read the file straight from storage without unpacking. Works on planes, in mountains, underground — no internet. Vector MBTiles beat raster — 5–10x more compact, client-side styling.

Self-hosted tile server. TileServer-GL (Node.js, open-source from MapTiler) launches via docker run -v $(pwd):/data maptiler/tileserver-gl my-map.mbtiles and immediately serves XYZ tiles + TileJSON + WMTS on port 8080. One MBTiles = one endpoint. Alternatives: mbtileserver (Go, single binary), Tileserver-PHP, mod_mbtiles for nginx. Fits teams that don't want to depend on cloud (Mapbox, MapTiler Cloud) and keep basemaps in-house.

Archival snapshot. A map inside a SQLite file is archival-ready: one file, validated through sqlite3 file.mbtiles "PRAGMA integrity_check", openable 20 years from now by the same sqlite3. Museums, archives, government open data portals use MBTiles for long-term snapshot storage (e.g. an OSM state on a specific date).

Mobile-app embed. A dev bundles MBTiles into Android assets/ or iOS Resources/. On first launch the app copies the file to writable storage and uses it through mapbox-gl-native or MapLibre Native. The .mbtiles size directly impacts APK/IPA size — typical city-level vector MBTiles 20–100 MB.

MBTiles vs PMTiles vs raw XYZ

Tile-container matrix:

  • MBTiles — single SQLite file. Random access via indexes, needs SQLite runtime, ideal for offline mobile and self-hosted tile servers. Inspect with sqlite3.
  • PMTiles — single binary file. HTTP range requests, ideal for serverless cloud hosting (S3/R2/CloudFlare Pages). Inspect with pmtiles CLI.
  • Raw XYZ directory — thousands of .png/.pbf files under z/x/y.ext. Simplest format, readable by any HTTP server. Downsides — millions of inodes, slow rsync, slow filesystem ops.

When to pick what: for offline mobile or enterprise self-hosting — MBTiles. For serverless cloud deployment (R2/S3/GitHub Pages) — PMTiles. For dev/debug — raw XYZ. MBTiles and PMTiles interconvert in minutes via pmtiles convert input.mbtiles output.pmtiles.

Strengths and weaknesses

Strengths. Single-file deployment. Universal mobile-app support (OSMAnd, Maps.me, Locus, OruxMaps, GAIA GPS — all read out of the box). Indexed random access — SQLite gives O(log n) lookup without full scans. Inspect-friendly — open sqlite3 file.mbtiles and see everything. Supports vector (MVT) and raster (PNG/JPEG/WebP). Open spec under BSD-2 (no royalty or vendor lock-in). Mature ecosystem (since 2011, battle-tested).

Weaknesses. Not optimized for cloud static hosting — needs runtime (TileServer-GL, mbtileserver) with SQLite library, can't just sit on S3 and serve range requests like PMTiles. SQLite WAL mode and large files (>10 GB) can hit performance issues with the default page cache. TMS vs XYZ Y-axis duality bites devs who didn't read the spec. No built-in streaming — inserting millions of tiles needs a single open transaction (otherwise fsync per row = SSD killer). Spec last updated 2016 — no official additions in 9 years; development goes through tippecanoe and TileServer-GL de-facto.

Pipeline in OSM2CDR

Our MBTiles exporter supports two modes:

Raster mode (default). Pipeline: 1. Compute tile pyramid. From bbox and minzoom/maxzoom (default 10..14) we calculate the list of (z, x, y) tiles. There is no tiny hard cap: the realistic scale is tens of thousands of tiles per file, and for a clearly unliftable pyramid (from ~50,000) the service warns before rendering and suggests shrinking the area or lowering maxzoom. 2. Render each tile. QGIS headless renders each tile as PNG 256×256 (or 512×512 for retina) applying the selected map style. 3. Insert into SQLite. Open the .mbtiles file, begin one BEGIN TRANSACTION, run INSERT INTO tiles VALUES (z, x, flip_y(y), blob), commit at the end as a batch. One transaction for the whole export — critical for speed (without it SQLite fsyncs on every INSERT). 4. Write metadata. INSERT into metadata: name, format=png, bounds, minzoom, maxzoom, attribution=© OpenStreetMap contributors, type=baselayer.

Vector mode (our own MVT encoder). Pipeline: 1. Layers from the render pipeline. roads/buildings/water/POI arrive as the same layers every other vector format of the service gets. 2. MVT encoding by our code. Tiles are computed by the service's own pure-Python encoder (the mvt_tile_core module shared with the MVT ZIP format), no external tools: each input layer becomes its own named MVT layer (roads/buildings/water — not a faceless "layer0"), each tile is encoded and gzip-compressed. Order budgets: up to 4096 tiles and up to 300,000 features — exceeding them fails honestly before encoding starts. 3. Packing into SQLite. MBTiles 1.3 schema: format=pbf plus a TileJSON json row in metadata — MapLibre, Maputnik, TileServer-GL and QGIS discover the source layers right away. 4. Verify. sqlite3 output.mbtiles "SELECT count(*) FROM tiles" — sanity check that tiles got written.

Output — an .mbtiles file ready for OSMAnd import (Documents → OsmAnd → tiles), Locus Map (Maps → External maps → Add map), TileServer-GL (docker run -v $(pwd):/data maptiler/tileserver-gl your.mbtiles).

Validate via sqlite3 your.mbtiles "PRAGMA integrity_check" — SQLite-level corruption check. For tile coverage — sqlite3 your.mbtiles "SELECT zoom_level, count(*) FROM tiles GROUP BY zoom_level".

Map generation of Vladivostok city center

FAQ

How does MBTiles differ from PMTiles? MBTiles — SQLite single-file, needs a SQLite runtime to read, ideal for offline mobile and self-hosted tile servers. PMTiles — binary single-file laid out for HTTP range requests, ideal for cloud serverless static hosting on S3/R2 with no server. They convert into each other in minutes via pmtiles convert. Pick MBTiles for mobile apps, PMTiles for web maps.

How to open MBTiles in QGIS? Layer → Add Layer → Add XYZ Layer → New → URL mbtiles:///absolute/path/to/file.mbtiles (three slashes for absolute path). Or just drag-and-drop the .mbtiles file onto the canvas — the GDAL MBTiles driver opens it automatically. QGIS 3.16+ supports MBTiles natively for raster and vector.

Can I use MBTiles on static hosting without a server? Not directly. SQLite needs a runtime — browsers can't open .mbtiles. Workaround: run sql.js or a similar WASM-port of SQLite in the browser (proof-of-concept projects exist), but that's slow and complex. The right solution — convert to PMTiles: pmtiles convert input.mbtiles output.pmtiles and upload to R2/S3.

What's the maximum MBTiles size? SQLite supports files up to 281 TB theoretically. Practically — smooth up to ~10 GB; beyond that the default page cache (2 MB) struggles and you need PRAGMA tuning (cache_size=-262144 for 256 MB cache). A whole-planet OSM vector MBTiles is around 80 GB, raster — terabytes. For cloud distribution of files >4 GB, PMTiles is the better choice.

Conclusion

MBTiles has served the offline-cartography industry for 14 years: 2 SQLite tables, an open spec, an ecosystem of 20+ compatible apps. On osm2cdr.ru you can get a ready MBTiles of your region via the MBTiles export page — pick bbox, zoom range, raster or vector mode, download the SQLite file and import into your mobile app or tile server. For static cloud hosting see PMTiles — it fits serverless deployment better.

Sources

← All articles