Comparing MBTiles and GeoPackage for Basemaps
If the artefact is only a Web Mercator raster basemap, MBTiles is smaller, simpler and supported by more tools. If it also has to carry vector layers, use a grid other than Web Mercator, or be a single self-contained deliverable, GeoPackage is the format that can do those things and MBTiles is not. Both are SQLite files holding tile BLOBs; the difference is entirely in what else they can express.
This page belongs to the Raster Tiles & Coverage in GeoPackage guide.
Why This Matters
The two formats look almost identical from the outside — a .mbtiles and a .gpkg are both SQLite databases containing images keyed by zoom, column and row — which makes it easy to treat the choice as arbitrary. It is not, and it is difficult to reverse: a pipeline, a client integration and a distribution process all get built around one, and switching later means rebuilding the artefact and changing the code that reads it.
The deciding question is almost always whether vector data travels with the basemap. That single requirement rules MBTiles out, because it has no concept of a feature table.
Prerequisites
- Familiarity with GeoPackage tile pyramids from the parent guide
- A clear statement of what the artefact must contain
- The client library’s format support, which is frequently the binding constraint
- GDAL 3.4+ if you want to convert between the two
Primary Method
# The same source, built both ways — start by measuring
gdal_translate -of MBTILES ortho.tif basemap.mbtiles \
-co TILE_FORMAT=JPEG -co QUALITY=80
gdaladdo -r average basemap.mbtiles 2 4 8 16 32
gdal_translate -of GPKG ortho.tif basemap.gpkg \
-co RASTER_TABLE=basemap -co TILE_FORMAT=JPEG -co QUALITY=80 \
-co TILING_SCHEME=GoogleMapsCompatible
gdaladdo -r average basemap.gpkg 2 4 8 16 32
ls -lh basemap.mbtiles basemap.gpkg
For a like-for-like Web Mercator pyramid the two land within a few per cent of one another, with MBTiles slightly smaller — it carries less metadata. The size difference is real and rarely decisive; the capability difference usually is.
Step-by-Step Walkthrough
1. Start from what has to be in the artefact
If the answer includes “and the survey layers”, the decision is made. MBTiles has a tiles table, a metadata table of key-value strings, and nothing else — there is no place for a feature table, an attribute schema, or a spatial index. Shipping both means shipping two files that must stay in step, which is exactly the coordination problem a single container removes.
2. Check the client library
This is frequently the binding constraint and is worth checking before anything else. Support for MBTiles as a local basemap source is near-universal in mobile mapping SDKs; support for GeoPackage raster pyramids is common but not universal, and some libraries read GeoPackage features while ignoring its tiles.
# Inspect what a container actually offers, before assuming
import sqlite3
def describe(path: str) -> dict:
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
names = {r[0] for r in conn.execute(
"SELECT name FROM sqlite_master WHERE type IN ('table','view')"
)}
conn.close()
return {
"looks_like": "mbtiles" if "metadata" in names and "tiles" in names
else "geopackage" if "gpkg_contents" in names else "unknown",
"has_features": "gpkg_geometry_columns" in names,
"has_tiles": "gpkg_tile_matrix" in names or "tiles" in names,
}
3. Compare on the axes that matter
| Axis | MBTiles | GeoPackage |
|---|---|---|
| Tiling scheme | Web Mercator, fixed | Any, described by the matrix tables |
| Row direction | TMS, counting up | Counts down from the extent top |
| Vector features | Not supported | Feature tables with R-tree indexes |
| Metadata | Key-value strings | Typed registry tables |
| Governance | Community specification | OGC standard |
| Tooling | Very wide for basemaps | Wide, and broader for vector work |
The row-direction difference is worth noting explicitly because it is the one that produces a mirrored map when a pipeline treats the two as interchangeable. MBTiles uses the TMS convention; GeoPackage does not.
4. Convert when you have to
Conversion is straightforward in both directions because the tile payloads are identical — only the addressing and the metadata differ.
# MBTiles -> GeoPackage
gdal_translate -of GPKG basemap.mbtiles basemap.gpkg \
-co RASTER_TABLE=basemap -co TILING_SCHEME=GoogleMapsCompatible
gdaladdo -r average basemap.gpkg 2 4 8 16 32
# GeoPackage -> MBTiles (only possible for a Web Mercator pyramid)
gdal_translate -of MBTILES basemap.gpkg basemap.mbtiles
gdaladdo -r average basemap.mbtiles 2 4 8 16 32
The second direction only works if the pyramid is already in Web Mercator on the standard grid. A GeoPackage pyramid in a national grid has no MBTiles equivalent, and GDAL will reproject rather than fail — which is a much larger operation than the command implies.
5. Decide, and record why
Whichever way it goes, record the reason with the artefact. A future maintainer looking at a .mbtiles needs to know whether Web Mercator was a requirement or an accident, and whether the vector layers live elsewhere by design.
Verification
Confirm a converted container is genuinely equivalent rather than merely present.
# Tile counts per level must match across a conversion
import sqlite3
mb = sqlite3.connect("file:basemap.mbtiles?mode=ro", uri=True)
gp = sqlite3.connect("file:basemap.gpkg?mode=ro", uri=True)
mb_counts = dict(mb.execute(
"SELECT zoom_level, count(*) FROM tiles GROUP BY zoom_level"
))
gp_counts = dict(gp.execute(
"SELECT zoom_level, count(*) FROM basemap GROUP BY zoom_level"
))
assert mb_counts == gp_counts, f"tile counts differ:\n {mb_counts}\n {gp_counts}"
And check the row translation was applied, by comparing a specific tile’s bytes at a known address:
z, x = 12, 2045
(h,) = gp.execute(
"SELECT matrix_height FROM gpkg_tile_matrix "
"WHERE table_name='basemap' AND zoom_level=?", (z,)
).fetchone()
y_gp = 1362
y_mb = h - 1 - y_gp # TMS counts the other way
a = gp.execute(
"SELECT tile_data FROM basemap WHERE zoom_level=? AND tile_column=? AND tile_row=?",
(z, x, y_gp),
).fetchone()
b = mb.execute(
"SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?",
(z, x, y_mb),
).fetchone()
assert a and b and a[0] == b[0], "the row translation is wrong — tiles do not match"
Alternative Approaches or Edge Cases
PMTiles. A newer single-file format designed to be read directly over HTTP range requests, which removes the tile server entirely for a hosted basemap. It is not SQLite and does not carry features, so it competes with MBTiles rather than with GeoPackage, and it is a strong option where the basemap is served rather than shipped.
Both, deliberately. Producing an MBTiles for the mobile client and a GeoPackage for the desktop and archive is a legitimate arrangement when the two consumers genuinely differ. It costs a second build step and a second thing to keep in step, which is the trade to be explicit about.
Vector tiles in either. Both formats can carry vector tiles, and the comparison barely changes: MBTiles remains fixed to Web Mercator, GeoPackage remains able to carry ordinary feature tables alongside. Where vector tiles are the delivery mechanism, the choice is usually made by the client library.
Troubleshooting
A converted basemap renders mirrored
Cause: The row translation was not applied — the conventions differ and the tiles were copied by address rather than by position. Fix: Flip tile_row against matrix_height per level, as in the verification section. GDAL handles this correctly; hand-rolled conversions frequently do not.
The GeoPackage renders and the MBTiles does not
Cause: The pyramid is not in Web Mercator, so the MBTiles conversion either reprojected it or produced something with an implicit grid that does not match the data. Fix: Check the source pyramid’s srs_id; if it is not 3857, MBTiles is the wrong target.
A client sees the tiles but not the feature layers
Cause: The client reads GeoPackage tiles and not GeoPackage features, or the reverse. This is common and is not a container problem. Fix: Verify against the client’s documentation before choosing the format — the describe helper above tells you what the file offers, not what the client will use.
Frequently Asked Questions
Is MBTiles smaller for the same imagery?
Marginally — it carries less metadata and no registry tables, which on a pyramid of any size is a rounding error. If the two differ by more than a few per cent for the same source, the tile format or the maximum zoom differs between the builds, and that is worth finding rather than attributing to the container format.
Can a GeoPackage hold vector tiles and feature tables at once?
Yes, and that combination is one of the stronger arguments for the format: a single artefact carrying a rendered basemap, vector tiles for styling, and queryable feature tables with spatial indexes. MBTiles can hold the first two and not the third, which is the same capability gap in another form.
Which is better supported by mobile SDKs?
MBTiles, for raster basemaps, by a clear margin — it has been the de facto offline basemap format for long enough that support is close to universal. GeoPackage support is common and improving, and is often better for the vector side. Where the client is fixed and the format is not, let the client decide.
Related
- Raster Tiles & Coverage in GeoPackage — parent guide: the GeoPackage pyramid model
- Writing a Tile Pyramid into a GeoPackage — building the GeoPackage side of the comparison
- How to Serve GeoPackage Tiles Offline — where the row-convention difference bites
- SpatiaLite vs GeoPackage for Mobile Offline Sync — the same kind of decision on the vector side