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…

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

bash
# 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.

What each format can expressBoth formats store raster tiles in SQLite and both support vector tiles. MBTiles is fixed to the Web Mercator web-tile grid and has no concept of feature tables, attribute data or multiple layers of different kinds. GeoPackage supports any tiling scheme, carries feature tables with spatial indexes alongside the tiles, and is an OGC standard. The capability difference, not the size difference, is what decides.MBTilesGeoPackageraster tilesyesyestiling schemeWeb Mercator onlyany, via the matrix tablesfeature tablesnoneyes, with spatial indexesstandardisationa community specificationan OGC standardthe third row is the one that usually decides
The first row is why the two look interchangeable; the third is why they are not.

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.

python
# 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

AxisMBTilesGeoPackage
Tiling schemeWeb Mercator, fixedAny, described by the matrix tables
Row directionTMS, counting upCounts down from the extent top
Vector featuresNot supportedFeature tables with R-tree indexes
MetadataKey-value stringsTyped registry tables
GovernanceCommunity specificationOGC standard
ToolingVery wide for basemapsWide, 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.

bash
# 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.

What conversion between the two formats actually doesConverting MBTiles to GeoPackage is a re-addressing and a metadata rewrite: the tile payloads are copied unchanged and the row indices are flipped. Converting a Web Mercator GeoPackage to MBTiles is the same operation in reverse. Converting a GeoPackage pyramid in another reference system to MBTiles is a full reprojection and resample, which is a far larger operation than the command suggests.MBTiles → GeoPackagepayloads copied unchanged · rows flipped · metadata rewritten · fastWeb Mercator GeoPackage → MBTilesthe same operation in reverse · equally fastother-projection GeoPackage → MBTilesa full reprojection and resample · slow, lossy, and easy to invoke by accident
Two of the three are cheap re-addressings. The third looks identical on the command line and is a different operation entirely.

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.

python
# 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:

python
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"
The decision, in two questionsFirst: does anything other than raster tiles have to travel in the same file? If it does, GeoPackage is the only option, because MBTiles has no feature tables. If it does not, the second question is whether the pyramid must use a grid other than Web Mercator. If it must, GeoPackage again; if not, MBTiles is smaller, simpler and more widely supported for this one job.features in the same file?vector layers, attributesyesnoGeoPackagethe only optiona non-Mercator grid?a national schemeyes — GeoPackageno — MBTiles
Two questions, and the first one settles most cases before the second is reached.

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.