Using tippecanoe-decode to Inspect Build Output

tippecanoe-decode archive.mbtiles 14 8188 5449 prints one tile as GeoJSON with real coordinates restored. It is the shortest path from “the build succeeded” to “the build produced what I asked for”, and it answers four questions no build log can: which layers exist, which attributes survived, how many features are in a tile, and whether a zoom level contains anything at all.

When to Use This

Immediately after any change to a build’s flags, and whenever a map renders wrong and you need to know whether the fault is in the tiles or the style. It is also the check a CI gate should run on a sampled tile, because a structurally valid archive full of empty tiles passes every other test.

Specification Detail

Invocation What it prints
tippecanoe-decode file.mbtiles Every tile in the archive as one GeoJSON stream
tippecanoe-decode file.mbtiles Z X Y One tile, coordinates restored to EPSG:4326
tippecanoe-decode -c file.mbtiles Coordinates in tile-local units instead of degrees
tippecanoe-decode -s EPSG:3857 … Coordinates in the given projection
tippecanoe-decode -f … Continue past a corrupt tile rather than stopping

It reads .mbtiles and .pmtiles alike. Without a z x y it decodes the whole archive, which for anything but a tiny tileset is not what you want — always pass a tile address.

Build, decode one tile, compare against what you asked forA build produces an archive, a single tile is decoded to GeoJSON, its layers, attributes and feature count are compared against the build's flags, and any mismatch sends you back to the flags.THE LOOPBuildtippecanoePick a tiledense, mid-zoomthe largest tile findsthe mostDecodeone z/x/yComparelayers, attrs, countagainst the flags you passed
The loop closes in seconds. Every alternative — loading the tileset in a browser, deploying it, asking someone — takes minutes and tells you less.

Production Command

Pick the largest tile at a mid zoom — it exercises the most layers and the widest attribute coverage — then read the four things that matter:

bash
# 1. Find a tile worth looking at
sqlite3 build/basemap.mbtiles \
  "SELECT zoom_level, tile_column, tile_row, length(tile_data)
   FROM tiles WHERE zoom_level = 12
   ORDER BY length(tile_data) DESC LIMIT 1;"
# 12|2048|1362|318114

# MBTiles rows are TMS; tippecanoe-decode wants XYZ y = 2^z - 1 - row
Y=$(( (1 << 12) - 1 - 1362 ))

# 2. Layer names actually published
tippecanoe-decode build/basemap.mbtiles 12 2048 "$Y" \
  | jq -r '.features[].properties["tippecanoe:layer"] // empty' | sort -u

# 3. Attributes that survived filtering
tippecanoe-decode build/basemap.mbtiles 12 2048 "$Y" \
  | jq -r '[.features[].properties | keys[]] | unique | .[]'

# 4. Feature count per layer
tippecanoe-decode build/basemap.mbtiles 12 2048 "$Y" \
  | jq -r '.features[].properties["tippecanoe:layer"]' | sort | uniq -c

The TMS conversion on the second line is the step people skip, and it decodes a tile from the wrong hemisphere without complaining — the same y-axis flip that mirrors maps vertically elsewhere.

Wrapped as a gate that fails a build rather than a reader:

bash
#!/usr/bin/env bash
set -euo pipefail
ARCHIVE=$1; Z=$2; X=$3; Y=$4
EXPECTED_LAYERS=$(sort layers.manifest)

DECODED=$(tippecanoe-decode "$ARCHIVE" "$Z" "$X" "$Y")
ACTUAL=$(jq -r '.features[].properties["tippecanoe:layer"]' <<<"$DECODED" | sort -u)
COUNT=$(jq '.features | length' <<<"$DECODED")

diff <(echo "$EXPECTED_LAYERS") <(echo "$ACTUAL") \
  || { echo "layer set differs from the manifest"; exit 1; }
[ "$COUNT" -gt 0 ] || { echo "sampled tile is empty"; exit 1; }
echo "ok: $COUNT features across $(wc -l <<<"$ACTUAL") layers"
Four questions a build log cannot answerLayer names, attribute survival, feature counts and zoom occupancy, each with the failure it catches.WHAT ONE DECODE ANSWERSWhich layer names were publishedcatches a name taken from a filename instead of --layerWhich attributes survivedcatches an --include list that dropped something the style readsHow many features are in a real tilecatches a dropping strategy thinning far harder than intendedWhether a zoom level holds anythingcatches a minzoom that excluded the layer entirelyWhether coordinates land where expectedcatches a projection mistake before anyone opens a map
All four come from the same command. Together they cover the great majority of "the build worked but the map is wrong" reports.

Reading the Output

Two synthetic properties appear in the decoded GeoJSON and are worth knowing about. tippecanoe:layer carries the layer the feature came from, which is how a single stream of features stays attributable. tippecanoe:extent reports the tile’s coordinate grid, which confirms whether --detail did what you expected.

Coordinates are restored to EPSG:4326 by default, which makes them directly comparable with the source data — a quick jq on one feature’s first coordinate against the same feature in the source is the fastest projection check available.

Values slightly outside the tile’s geographic bounds are correct: they are the buffer strip carrying neighbouring geometry, and a decoder that flags them as errors is wrong.

What each verification method can actually seeFour ways to check a finished build compared on what each reveals about layers, attributes, feature counts and rendered output.WAYS TO CHECK A BUILDlayersattributesfeature countsrenderingBuild logIntendedNoWarnings onlyNoArchive metadataDeclaredDeclaredNoNotippecanoe-decodeActualActualActualNoLoading it in a browserActualIndirectNoYes
Only the decode sees the bytes a client will receive. The build log reports intentions and the archive metadata reports declarations.

Interaction Effects

With attribute filtering. The decoded property keys are the definitive answer to what --include and -x actually did. Reading the flags is not the same thing, because an -x naming an attribute that does not exist is silently ignored.

With dropping strategies. Comparing feature counts between a tile built with and without --drop-densest-as-needed quantifies what the strategy removed, which the build log reports only as a warning count.

With the style contract. The layer names and attribute keys from a decode are exactly what a style’s source-layer and get expressions must match — the same schema a style validation workflow checks against.

With PMTiles. tippecanoe-decode reads PMTiles directly, so the same command works after conversion. Useful for confirming that a conversion preserved everything.

Performance Impact

Decoding one tile is milliseconds and reads only that tile’s bytes, so it is safe to run against a large archive and cheap enough for every CI run. Decoding a whole archive is a different proposition: it materialises every feature as JSON, which for a 4-million-tile basemap is hundreds of gigabytes of output and hours of work. Always pass a tile address.

For a broader sample without the cost, decode a handful of tiles chosen by size across several zooms — five tiles catch nearly everything a full decode would.

Common Mistakes

Forgetting the TMS conversion. Decodes a tile from the mirrored latitude and produces confusing but internally consistent output.

Decoding a whole archive by accident. Omitting z x y starts a full decode, which on a large tileset fills a disk.

Sampling an empty tile. A tile at the edge of the extent may genuinely hold nothing. Pick the largest tile at the zoom, not an arbitrary one.

Treating buffer coordinates as errors. Values just outside the tile’s bounds are the buffer working correctly.

FAQ

Does it work on PMTiles?

Yes, with the same syntax. The tile addressing is XYZ in both cases, so no TMS conversion is needed when reading a PMTiles archive directly.

Why do some features have no tippecanoe:layer?

Because it is only emitted when decoding an archive containing several layers. A single-layer tileset omits it, and the layer name is in the archive metadata instead.

Can I decode a tile fetched over HTTP?

Not directly, but pmtiles tile will fetch one from a remote archive and pipe it to a decoder. For a z/x/y tile server, curl the tile and decode with a library — see decoding MVT tiles in Python.

Is the output valid GeoJSON?

It is a FeatureCollection per tile, with the synthetic tippecanoe: properties added. Strip those and it feeds any GeoJSON consumer directly.