How to Inspect PMTiles Metadata with CLI Tools
Run pmtiles show archive.pmtiles to dump the complete JSON header from any PMTiles archive — exposing tile type, compression algorithm, zoom range, bounding coordinates, and the embedded metadata object — without loading the entire file into RAM.
When to Use This
Use pmtiles show in these specific situations:
- Post-build validation: confirm that a freshly generated archive has the expected
tileType,tileCompression, and zoom range before uploading to cloud storage. - Debugging decoder errors: a tile server that refuses to serve tiles often fails because the declared
tileCompressiondoes not match what the server expects — inspection reveals the mismatch immediately. - CI/CD gates:
pmtiles showoutput is JSON, sojq -ecan fail a pipeline automatically when archive properties drift from spec. - Remote archive triage: the CLI accepts HTTP/HTTPS URLs and fetches only the header bytes via HTTP range requests, so you can inspect a live archive on S3 or Cloudflare R2 without downloading it.
Do not use pmtiles show as a substitute for tile-level validation — it reads the header and metadata sections only, not individual tile payloads. For payload-level checks, use pmtiles verify or parse tiles with the Python pmtiles library.
Specification Detail: Header Fields Returned by pmtiles show
The tool reads the 127-byte fixed header defined in the PMTiles v3 spec and surfaces its fields as camelCase JSON keys. The integer enum fields are the most frequently misread:
tileType enum:
| Value | Meaning | Content-Type header |
|---|---|---|
0 |
Unknown / Other | — |
1 |
MVT vector tile | application/x-protobuf |
2 |
PNG | image/png |
3 |
JPEG | image/jpeg |
4 |
WebP | image/webp |
5 |
AVIF | image/avif |
tileCompression / internalCompression enum:
| Value | Meaning | Decompress with |
|---|---|---|
0 |
Unknown | — |
1 |
None (uncompressed) | no-op |
2 |
gzip | gunzip / zlib |
3 |
Brotli | brotli -d |
4 |
Zstandard (zstd) | zstd -d |
internalCompression applies to the directory index sections; tileCompression applies to individual tile payloads. Both fields must match your tile server’s decoder configuration or every tile request will return a decode error.
Coordinate fields use E7 encoding in the raw binary header (integer value = coordinate × 10,000,000), but pmtiles show converts them to decimal degrees in the JSON output, matching the [min_lon, min_lat, max_lon, max_lat] convention used in TileJSON.
Installation
The official CLI is a single Go binary from protomaps/go-pmtiles. No runtime dependencies are required.
# Linux (x86_64) — replace VERSION with the latest release tag
VERSION=1.22.0
curl -L "https://github.com/protomaps/go-pmtiles/releases/download/v${VERSION}/go-pmtiles_${VERSION}_Linux_x86_64.tar.gz" \
| tar xz pmtiles
sudo mv pmtiles /usr/local/bin/
# macOS (Homebrew)
brew install protomaps/homebrew-tools/go-pmtiles
# Verify
pmtiles --version
The binary validates the PMTiles v3 magic bytes (PMTiles at offset 0–6) before reading any other field; an archive produced by an older tool or a truncated upload will fail immediately with a clear error.
Production Command
A single block for inspecting a local archive, extracting only the fields relevant to pipeline validation, and asserting the expected values:
#!/usr/bin/env bash
set -euo pipefail
ARCHIVE="dist/world.pmtiles"
# --- Dump full header for logging ---
echo "=== PMTiles header ===" >&2
pmtiles show "$ARCHIVE" | jq . >&2
# --- Extract key fields ---
HEADER=$(pmtiles show "$ARCHIVE")
TILE_TYPE=$(echo "$HEADER" | jq -r '.tileType')
TILE_COMP=$(echo "$HEADER" | jq -r '.tileCompression')
MIN_ZOOM=$(echo "$HEADER" | jq -r '.minZoom')
MAX_ZOOM=$(echo "$HEADER" | jq -r '.maxZoom')
BOUNDS=$(echo "$HEADER" | jq -r '.bounds | @json')
VEC_LAYERS=$(echo "$HEADER" | jq -r '.metadata.vector_layers | length')
echo "tileType=${TILE_TYPE} tileCompression=${TILE_COMP} zoom=${MIN_ZOOM}-${MAX_ZOOM}"
echo "bounds=${BOUNDS}"
echo "vector_layers=${VEC_LAYERS}"
# --- Hard assertions ---
# tileType=1 (MVT), tileCompression=2 (gzip)
echo "$HEADER" | jq -e '.tileType == 1 and .tileCompression == 2' > /dev/null \
|| { echo "ERROR: unexpected tileType or tileCompression"; exit 1; }
echo "$HEADER" | jq -e '.minZoom <= .maxZoom and .maxZoom <= 16' > /dev/null \
|| { echo "ERROR: zoom range invalid or exceeds z16"; exit 1; }
echo "$HEADER" | jq -e '(.bounds[2] - .bounds[0]) > 0 and (.bounds[3] - .bounds[1]) > 0' > /dev/null \
|| { echo "ERROR: degenerate bounding box"; exit 1; }
echo "Validation passed." >&2
To inspect a remote archive without downloading it, replace "dist/world.pmtiles" with the HTTPS URL:
pmtiles show "https://data.example.com/tiles/world.pmtiles" | jq '{tileType,tileCompression,minZoom,maxZoom,bounds}'
The CLI issues a single HTTP range request for the 127-byte header, then a second range request for the root directory section — typically under 5 KB total for the metadata fetch.
SVG: What pmtiles show Actually Reads
The diagram below shows which byte ranges the CLI fetches from the archive and what each section contains.
Python Automation
For pipelines that already use Python, the pmtiles PyPI package exposes the same header fields via memory-mapped I/O, avoiding subprocess overhead:
pip install pmtiles
from pmtiles.reader import Reader, MmapSource
import json, sys
def inspect_pmtiles(filepath: str) -> dict:
"""Parse PMTiles v3 header and metadata without loading the full archive."""
try:
with open(filepath, "r+b") as f:
source = MmapSource(f)
reader = Reader(source)
header = reader.header()
metadata = json.loads(reader.metadata()) # raw JSON string → dict
# Coordinates are E7-encoded in the binary header;
# the Python reader exposes them already divided by 1e7.
bounds = [
header["min_lon_e7"] / 1e7,
header["min_lat_e7"] / 1e7,
header["max_lon_e7"] / 1e7,
header["max_lat_e7"] / 1e7,
]
return {
"spec_version": header["spec_version"],
"tile_type": header["tile_type"], # integer enum
"tile_compression": header["tile_compression"], # integer enum
"min_zoom": header["min_zoom"],
"max_zoom": header["max_zoom"],
"bounds": bounds,
"metadata_keys": list(metadata.keys()),
"vector_layers": metadata.get("vector_layers", []),
}
except FileNotFoundError:
print(f"ERROR: {filepath} not found", file=sys.stderr)
sys.exit(1)
except Exception as exc:
print(f"ERROR: inspection failed — {exc}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "dist/world.pmtiles"
print(json.dumps(inspect_pmtiles(path), indent=2))
Call json.loads() on reader.metadata() before accessing nested keys — the reader returns a raw JSON string, not a parsed object. Accessing metadata["vector_layers"] directly on the string raises a TypeError that can be difficult to trace.
Interaction Effects
With zoom-level planning. The minZoom and maxZoom fields from pmtiles show should match the -Z and -z flags passed to Tippecanoe during generation. A mismatch indicates a packaging step silently dropped zoom levels. Cross-reference with calculating optimal max zoom for urban datasets when setting generation parameters.
With MBTiles migration. When migrating a SQLite-backed archive to PMTiles, run pmtiles show on the converted file and compare tileType and tileCompression against the source. The pmtiles convert tool preserves compression by default, but pipelines that re-encode tiles may silently change the compression codec — a discrepancy visible only via header inspection. The MBTiles architecture limits page covers what changes during conversion.
With CDN configuration. The bounds field from pmtiles show must fall within the geographic scope configured in your tile server or CDN routing rules. An archive covering a global dataset ([-180, -85.05, 180, 85.05]) served through a CDN origin restricted to a single region will silently return 404s for out-of-region tile requests. Inspect bounds before configuring cache rules.
Performance Impact
pmtiles show on a local file completes in under 50 ms for any archive size — it reads exactly 127 bytes plus the directory section (typically 512 bytes to several KB for dense global tilesets). The tool never loads tile payload data.
For remote archives over HTTPS, latency is dominated by TCP round trips, not data transfer volume. Two sequential range requests are made: one for the 127-byte header, one for the root directory plus metadata block. On a low-latency CDN origin this totals under 200 ms. On a high-latency origin (cross-region S3 with no CDN in front), expect 500–1500 ms — acceptable for CI/CD validation, not for runtime map rendering.
Inspecting tile count and coverage density requires pmtiles verify, which reads the full directory index and is O(n) in the number of tiles. For a global z14 tileset with ~340 million tiles, pmtiles verify can take several minutes.
Common Mistakes
1. Misreading tileType and tileCompression as strings
pmtiles show returns integer values for enum fields, not human-readable strings. A jq expression like .tileType == "MVT" silently evaluates to false; the correct assertion is .tileType == 1. Symptom: jq -e exits 0 but the pipeline passes an archive with the wrong tile type.
Corrective check:
pmtiles show archive.pmtiles | jq -e '.tileType == 1' \
|| echo "ERROR: archive is not MVT (tileType must be 1)"
2. Inspecting metadata before the archive is fully written
Running pmtiles show on an in-progress pmtiles convert or tippecanoe output gives a partially written file. The header magic bytes may be valid but meta_length reads as 0, and reader.metadata() in Python returns an empty string. Always verify the process writing the archive has exited before inspecting.
Symptom: json.loads(reader.metadata()) raises json.JSONDecodeError: Expecting value: line 1 column 1 (char 0).
# Wait for the generating process and inspect only on clean exit
tippecanoe -o dist/world.pmtiles input.geojson && pmtiles show dist/world.pmtiles | jq .
3. Confusing internalCompression with tileCompression
internalCompression (offset 97 in the binary header) controls how the directory index is compressed. tileCompression (offset 98) controls individual tile payloads. The pmtiles show JSON output labels these distinctly, but code that reads the raw binary header by offset position can swap them. A tile server configured to decompress tile payloads with zstd but receiving a gzip-compressed archive will emit decode errors on every tile request.
Corrective check against both fields:
pmtiles show archive.pmtiles | jq '{internalCompression, tileCompression}'
Up: PMTiles Specification Deep Dive
Related
- PMTiles Specification Deep Dive — the full binary layout, Hilbert curve tile IDs, and directory indexing that
pmtiles showsurfaces. - MBTiles Architecture & Limits — SQLite-backed tile storage constraints that PMTiles was designed to eliminate; relevant when validating converted archives.
- Calculating Optimal Max Zoom for Urban Datasets — cross-reference the
maxZoomfrompmtiles showagainst generation strategy to catch zoom truncation early. - Essential Tippecanoe Flags for Production Builds — the
-Z/-zflags that determine the zoom range recorded in the PMTiles header.