Converting MBTiles to PMTiles with pmtiles convert

pmtiles convert basemap.mbtiles basemap.pmtiles reorders every tile onto a Hilbert curve, deduplicates identical tiles by content hash, copies the metadata, and writes a single archive that object storage can serve with range requests. It is lossless with respect to tile bytes and it is not lossless with respect to everything else — the details below are the ones worth knowing before publishing the output.

When to Use This

Convert when a tileset that already builds as MBTiles needs to be served without a tile server. The common cases are moving a working pipeline onto object storage, publishing a snapshot of a database-backed tileset, or distributing a tileset to consumers who should not need infrastructure to read it.

Do not convert as a routine build step if Tippecanoe can write PMTiles directly — it can, since 2.17, with --output basemap.pmtiles — because a direct write skips an entire read-and-rewrite pass over the whole archive.

Specification Detail

Aspect MBTiles input PMTiles output
Tile order on disk Insertion order Hilbert curve order
Duplicate tiles Stored per row unless the images/map schema is used Deduplicated by content hash, always
Metadata metadata table, json row Metadata section, offset in the header
Tile compression Whatever the rows hold Preserved, recorded in the header
Row addressing TMS (tile_row counts north) Normalised to XYZ internally

Two of these deserve attention. Deduplication is automatic and often dramatic: a tileset with large uniform areas — ocean, empty land, a single-colour landcover fill — can shrink by half, because every identical tile becomes one byte range referenced many times. And the TMS-to-XYZ normalisation means the conversion silently fixes an axis convention that MBTiles readers had to handle themselves.

What the converter does in one passTiles are read from the SQLite index, hashed and deduplicated, sorted into Hilbert order, written sequentially, and the directory is built from the resulting offsets.CONVERSIONReadSQLite indexin row orderHashdedupe identicaloften halves anocean-heavy tilesetSortHilbert tile idWritesequential blobsDirectoryoffsets + lengths
The Hilbert sort is what makes range coalescing work later: tiles that are near each other on the map end up near each other in the file.

Production Command

bash
# Install the single Go binary
curl -sL https://github.com/protomaps/go-pmtiles/releases/latest/download/go-pmtiles_Linux_x86_64.tar.gz \
  | tar xz -C /usr/local/bin pmtiles

# Convert, then verify before anything is uploaded
pmtiles convert basemap.mbtiles basemap.pmtiles
pmtiles verify basemap.pmtiles

# Compare what the two archives claim about themselves
sqlite3 basemap.mbtiles \
  "SELECT name, value FROM metadata WHERE name IN ('minzoom','maxzoom','format');"
pmtiles show basemap.pmtiles | jq '{min_zoom, max_zoom, tile_type, tile_compression, addressed_tiles_count, tile_contents_count}'

addressed_tiles_count against tile_contents_count is the deduplication ratio, and it is the single most informative number the conversion produces. A basemap covering ocean might report 4.2 million addressed tiles and 1.6 million distinct contents — a 62% reduction that costs nothing.

Then upload with the headers range delivery needs:

bash
aws s3 cp basemap.pmtiles s3://tiles-bucket/v43/basemap.pmtiles \
  --content-type "application/octet-stream" \
  --cache-control "public, max-age=31536000, immutable"

# Confirm the object answers ranges before pointing a style at it
curl -sI -r 0-99 "https://tiles.example.com/v43/basemap.pmtiles" | head -1
# HTTP/2 206
What deduplication removes, by tileset shapeFour tilesets compared before and after conversion: a global basemap with ocean, a country basemap, a dense city extract and a fully populated raster-like tileset.DEDUPLICATIONGB after conversionGlobal basemap (much ocean)38 GB from 96Country basemap12 GB from 17Dense city extract1.4 GB from 1.4Uniformly populated tileset22 GB from 23
The saving tracks how much of the world is identical. A city extract has almost no duplicate tiles and gains nothing from this step.
What the conversion changes about the same tilesetFive properties compared between an MBTiles input and its PMTiles output: tile order, duplicate handling, metadata location, row addressing and what it takes to serve.BEFORE AND AFTERMBTiles inPMTiles outTile order on diskInsertion orderHilbert curve orderDuplicate tilesStored per rowDeduplicated by hashMetadata locationmetadata tableHeader-addressedsectionRow addressingTMSNormalised to XYZTo serve itA tile serverObject storage
The tiles are byte-identical. Everything about how they are found changes, which is the entire point of the conversion.

Interaction Effects

With metadata. The converter copies what it finds. An MBTiles archive with a missing vector_layers produces a PMTiles archive with the same gap, and patching it after conversion is more awkward than fixing the source. Check before converting.

With versioned publishing. The output is one object, so it slots directly into a versioned prefix scheme. It also means a single-tile fix is impossible — the archive is rewritten and republished as a whole.

With tile-join. Merge first, convert last. Converting each input and merging afterwards is not possible; tile-join reads MBTiles, so the conversion belongs at the end of the layer composition chain.

Performance Impact

The conversion is a full read and a full write, so it is bounded by disk throughput and the sort. Measured on an NVMe workstation:

Tileset Tiles Convert time Peak RAM
City extract 0.4 M 24 s 380 MB
Country basemap 4 M 4 m 10 s 1.1 GB
Global basemap 40 M 46 m 3.4 GB

Memory is dominated by the tile-id-to-offset table, which is roughly proportional to the deduplicated tile count. Disk headroom matters more than RAM: the conversion needs room for both archives at once.

Common Mistakes

Converting on every build. If Tippecanoe can write PMTiles directly, the conversion pass is pure overhead — a full read and write of the whole tileset for no change in content.

Uploading with the wrong content type. application/octet-stream is correct. A type the CDN considers compressible invites a body transform that invalidates every offset in the directory.

Skipping pmtiles verify. It reads the directory and confirms every entry resolves, which catches a truncated write before it becomes a 416 in production.

Deleting the MBTiles immediately. It is the only artefact that can be edited tile by tile. Keep it until the converted archive has been serving successfully.

FAQ

Is the conversion lossless?

For tile bytes, yes — each tile’s blob is copied unchanged, and deduplication only removes exact duplicates. Tile ordering and the internal addressing change completely, and anything that depended on MBTiles’ SQLite structure no longer applies.

Can I convert back?

Yes, pmtiles convert accepts a .pmtiles input and writes .mbtiles. The round trip preserves tiles and metadata, but the deduplication is undone — every addressed tile becomes its own row again, so the output is the original size.

Does converting change tile content?

No. It does not re-encode, re-compress or re-simplify anything. If a tile is over budget in the MBTiles it is over budget in the PMTiles, and the fix belongs in the tile build.

What happens to a tileset with mixed compression?

The header records one compression for the whole archive, so mixed inputs are a problem. Normalise before converting — an archive with some gzipped and some raw tiles produces an output that no client can read consistently.