Automated Generation Pipelines with Tippecanoe

Tippecanoe converts GeoJSON, GeoParquet, and other spatial formats into Mapbox Vector Tiles (MVT) across a configured zoom range, applying feature dropping, geometry simplification, and attribute filtering to keep each tile under the 500 KB budget. Automating this process — wiring Tippecanoe into repeatable, version-controlled workflows — removes the operational overhead of manual regeneration and makes spatial datasets deployable artifacts on the same cadence as application code.

Pipeline Architecture: GeoJSON to CDN

The diagram below maps the canonical five-stage pipeline from raw source data through CDN delivery. Every automated setup, regardless of orchestrator, passes through these stages in order.

The stages an automated Tippecanoe build runs on every commitSource data is extracted and validated, streamed into Tippecanoe, packed into a PMTiles or MBTiles archive, gated against the tile size budget, and published under a versioned prefix.BUILD PIPELINEExtractPostGIS · GeoParquetogr2ogr · DuckDBValidateCRS, topologyattribute typescheap here, expensivelatertippecanoe-z14 --drop-densestlayers assignedGatemax tile bytesfeature countsfails the build, notthe readerPublishversioned prefiximmutable
The gate between build and publish is what makes the pipeline automatable. Without it, a bad build reaches readers and the rollback is manual.

Each stage is independently testable. Separating ingestion from tile compilation lets teams run Tippecanoe in parallel across datasets while validating upstream schema changes in isolation.

Core Tippecanoe Flag Taxonomy

The full Tippecanoe CLI flag set is large, but production pipelines rely on a well-defined core group. Misunderstanding these flags is the most common source of oversized tiles or incorrect zoom coverage.

Flag Purpose Typical production value
-z / --maximum-zoom Highest zoom level to generate 14 for urban datasets; 10 for country-scale
-Z / --minimum-zoom Lowest zoom level (default 0) 02
-e / --output-to-directory Write tiles to a directory instead of MBTiles Used for PMTiles conversion step
-o / --output Write directly to .mbtiles or .pmtiles output.pmtiles
-l / --layer Name the output layer Must match the style spec layer source-layer
-y / --include Allowlist of attribute columns to retain Keep only render-relevant fields
--drop-densest-as-needed Drop features at low zoom to meet tile size budget Enabled for point datasets
--simplification Simplification level in tile coordinate units (default 2) 210 depending on geometry complexity
--no-tile-size-limit Disable the 500 KB hard cap Use only with PMTiles on CDN range-request delivery
--force Overwrite existing output file Required in CI where artifacts persist across runs
--read-parallel Process multiple input files in parallel Speeds up multi-layer builds
--extend-zooms-if-still-dropping Auto-raise max zoom until no features are dropped Useful for dense datasets where dropping is unacceptable

For a complete annotated inventory of flags that affect production builds, see Essential Tippecanoe Flags for Production Builds.

Implementation Patterns

Pattern 1: Single-Dataset Bash Pipeline

The simplest reproducible pipeline wraps Tippecanoe in a Bash script that validates inputs, runs conversion, and writes output to a versioned path.

bash
#!/usr/bin/env bash
set -euo pipefail

INPUT="data/admin_boundaries.geojson"
OUTPUT="dist/admin_boundaries_v$(date +%Y%m%d).pmtiles"
LAYER="admin"
MAX_ZOOM=10

# Validate source exists and is non-empty
[ -s "$INPUT" ] || { echo "ERROR: $INPUT missing or empty"; exit 1; }

tippecanoe \
  --output="$OUTPUT" \
  --layer="$LAYER" \
  --minimum-zoom=0 \
  --maximum-zoom="$MAX_ZOOM" \
  --simplification=4 \
  --include=iso_a2 \
  --include=name \
  --include=admin_level \
  --drop-densest-as-needed \
  --force \
  "$INPUT"

echo "Generated: $OUTPUT ($(du -sh "$OUTPUT" | cut -f1))"

Every flag is explicit — no reliance on defaults that may change across Tippecanoe versions. The versioned filename (_v20260624.pmtiles) enables safe atomic CDN swaps.

Pattern 2: Multi-Layer Python Subprocess Build

When a tileset requires merging several source layers (roads, parcels, buildings), Python’s subprocess module produces a maintainable build script with per-layer configuration:

python
#!/usr/bin/env python3
import subprocess
import sys
from pathlib import Path

LAYERS = [
    {
        "input": "data/roads.geojson",
        "layer": "roads",
        "max_zoom": 14,
        "include": ["highway", "name", "oneway"],
    },
    {
        "input": "data/buildings.geojson",
        "layer": "buildings",
        "max_zoom": 16,
        "include": ["height", "building"],
    },
    {
        "input": "data/landuse.geojson",
        "layer": "landuse",
        "max_zoom": 12,
        "include": ["landuse", "name"],
    },
]

OUTPUT = Path("dist/composite.pmtiles")
OUTPUT.parent.mkdir(parents=True, exist_ok=True)

cmd = [
    "tippecanoe",
    f"--output={OUTPUT}",
    "--minimum-zoom=0",
    "--simplification=3",
    "--drop-densest-as-needed",
    "--force",
    "--read-parallel",
]

for layer in LAYERS:
    cmd += [
        f"--layer={layer['layer']}",
        f"--maximum-zoom={layer['max_zoom']}",
    ]
    for attr in layer["include"]:
        cmd += [f"--include={attr}"]
    cmd.append(layer["input"])

result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
    print(result.stderr, file=sys.stderr)
    sys.exit(result.returncode)

size_kb = OUTPUT.stat().st_size / 1024
print(f"Built {OUTPUT} ({size_kb:.0f} KB)")

Passing --read-parallel with multiple input files lets Tippecanoe parallelise I/O across the source datasets. Each layer’s --include list is explicit — dropping unused attributes at build time rather than relying on the style spec to hide them produces measurably smaller tiles.

Pattern 3: GeoParquet Ingestion via DuckDB

For large datasets stored as GeoParquet, streaming through DuckDB avoids loading the full file into memory and integrates spatial filtering before tile generation:

bash
# Stream a bbox-filtered GeoParquet file to Tippecanoe via NDJSON
duckdb -c "
  LOAD spatial;
  COPY (
    SELECT
      ST_AsGeoJSON(geometry) AS geometry,
      name,
      population,
      admin_level
    FROM read_parquet('data/world_cities.geoparquet')
    WHERE ST_Within(geometry, ST_MakeEnvelope(-180, -60, 180, 85))
      AND population > 50000
  ) TO '/dev/stdout' (FORMAT JSON, ARRAY false);
" | tippecanoe \
    --output=dist/cities.pmtiles \
    --layer=cities \
    --minimum-zoom=2 \
    --maximum-zoom=12 \
    --include=name \
    --include=population \
    --drop-densest-as-needed \
    --force \
    -

# Verify output size
python3 -c "
import sqlite3, sys
conn = sqlite3.connect('dist/cities.pmtiles')  # pmtiles also readable as sqlite
# For pmtiles use pmtiles CLI instead
"

The - argument instructs Tippecanoe to read NDJSON from stdin. Converting large GeoParquet files to vector tiles covers the full streaming approach including column projection pushdown for datasets exceeding available RAM.

Performance and Scale Considerations

Tile Size Budgets

What each dataset type costs at its worst tileFive dataset types measured at the zoom where their tiles peak, against the 500 KB budget: administrative boundaries, road networks, landcover, building footprints and address points.BUDGET BY DATASETKB at the dataset's peak zoomAdmin boundaries, z860 KBLandcover, z12180 KBRoad network, z13340 KBBuilding footprints, z14700 KBAddress points, z15920 KB500 KB budget
Only the bottom two need dropping strategies at all. Applying --drop-densest-as-needed to a boundaries build costs fidelity and buys nothing.

The 500 KB per-tile limit is the practical ceiling for CDN delivery and browser parsing. Exceeding it degrades initial map load times and may trigger client-side memory pressure on mobile devices.

Dataset type Recommended max zoom Expected tile size at max zoom Primary control flag
Country/continent polygons z8–z10 50–200 KB --simplification=6
Admin boundaries (country level) z10–z12 100–400 KB --simplification=4
Urban road network z14 200–500 KB --include allowlist
Building footprints z16 100–350 KB --drop-densest-as-needed
Point of interest dataset z14 50–150 KB --drop-densest-as-needed

At zoom levels where feature density exceeds the tile budget, Tippecanoe drops features by default (--drop-densest-as-needed) or halts with an error if --no-tile-size-limit is not set. For datasets where dropping is unacceptable — parcel boundaries, utility networks — use --extend-zooms-if-still-dropping to automatically raise max-zoom until all features fit.

Zoom level optimization strategies provide quantified guidance on calculating the right --maximum-zoom value for a given dataset’s feature density, including the formula for calculating optimal max zoom for urban datasets.

Memory and CPU Constraints

Tippecanoe holds the full input dataset in memory during the sort phase before tile compilation. For datasets larger than available RAM, use one of these strategies:

bash
# Split large file by geography before merging
ogr2ogr -clipsrc -180 -60 0 85 data/world_west.geojson data/world.geojson
ogr2ogr -clipsrc 0 -60 180 85 data/world_east.geojson data/world.geojson

tippecanoe \
  --output=dist/world.pmtiles \
  --layer=world \
  --maximum-zoom=10 \
  --force \
  data/world_west.geojson data/world_east.geojson

# Or stream GeoParquet directly (avoids full in-memory load)
# See Pattern 3 above

Containerized CI runners should allocate at minimum 4 GB RAM for city-scale datasets at z14, and 16 GB for national road networks. Set ulimit -v in the CI environment to surface memory exhaustion early with a clear error rather than silent OOM kills.

Geometry Simplification Trade-offs

Tippecanoe’s built-in --simplification applies Douglas-Peucker reduction in tile coordinate space. For datasets with shared boundaries — municipal regions, land-use parcels — pre-processing with ST_SimplifyPreserveTopology in PostGIS preserves adjacency that Douglas-Peucker applied independently per feature will break.

The geometry simplification algorithms reference covers both Tippecanoe’s native approach and how Visvalingam-Whyatt compares to Douglas-Peucker for different geometry types. The short version: Visvalingam-Whyatt preserves visually meaningful vertices better for irregular organic shapes; Douglas-Peucker is faster and Tippecanoe’s default.

Storage and Delivery

MBTiles vs PMTiles

Both formats package tiles into a single file, but their delivery models differ in ways that matter for automated pipelines.

Property MBTiles (.mbtiles) PMTiles (.pmtiles)
Container SQLite database Custom binary archive
HTTP delivery Requires tile server (martin, tileserver-gl) Direct HTTP range requests from S3/R2/GCS
Serverless compatibility No Yes
Random tile access Via SQL query Via byte-range offset index
Tooling maturity High (SQLite ecosystem) Growing (pmtiles CLI, MapLibre native support)
Best for Local dev, offline apps, PostGIS workflows CDN delivery, serverless, edge deployments

For automated pipelines targeting web delivery, PMTiles is the preferred output format. The output is a single file: atomic to upload, trivial to version, and readable by MapLibre GL JS without an intermediate server. The MBTiles architecture and limits reference covers the SQLite concurrency constraints that surface in parallel pipeline runs.

CDN Cache-Control Strategy

Versioned tile artifacts eliminate cache invalidation complexity. Use a datestamp or content hash in the filename, then configure Cache-Control: public, max-age=31536000, immutable on object storage:

bash
# Upload PMTiles to Cloudflare R2 with immutable cache headers
OUTPUT_FILE="dist/admin_v20260624.pmtiles"
R2_BUCKET="my-tiles-bucket"
R2_PREFIX="tiles/admin/v20260624/"

# Using wrangler (Cloudflare R2)
wrangler r2 object put "${R2_BUCKET}/${R2_PREFIX}admin.pmtiles" \
  --file="$OUTPUT_FILE" \
  --content-type="application/octet-stream" \
  --cache-control="public, max-age=31536000, immutable"

# Using AWS CLI (S3-compatible)
aws s3 cp "$OUTPUT_FILE" \
  "s3://${R2_BUCKET}/${R2_PREFIX}admin.pmtiles" \
  --cache-control "public, max-age=31536000, immutable" \
  --content-type "application/octet-stream"

For frequently updated layers (daily sensor data, live transit), use max-age=3600 and a shorter version prefix. Keep the previous version live until client cache TTLs expire to avoid serving a mix of old style specs with new tile data.

CI/CD Integration

GitHub Actions Workflow

The minimal production GitHub Actions configuration runs Tippecanoe in a containerized environment with pinned Tippecanoe version:

yaml
name: Generate vector tiles

on:
  push:
    paths:
      - 'data/**/*.geojson'
      - 'data/**/*.geoparquet'
  schedule:
    - cron: '0 3 * * 1'  # Weekly Monday 03:00 UTC

jobs:
  build-tiles:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/felt/tippecanoe:latest

    steps:
      - uses: actions/checkout@v4

      - name: Validate inputs
        run: |
          for f in data/*.geojson; do
            python3 -c "import json,sys; json.load(open('$f'))" \
              || { echo "Invalid JSON: $f"; exit 1; }
          done

      - name: Generate tiles
        run: |
          tippecanoe \
            --output=dist/output.pmtiles \
            --layer=features \
            --minimum-zoom=0 \
            --maximum-zoom=14 \
            --include=name \
            --include=category \
            --drop-densest-as-needed \
            --force \
            data/*.geojson

      - name: Validate tile output
        run: |
          SIZE=$(stat -c%s dist/output.pmtiles)
          echo "Output size: ${SIZE} bytes"
          [ "$SIZE" -gt 1000 ] || { echo "ERROR: output suspiciously small"; exit 1; }

      - name: Upload to R2
        env:
          CLOUDFLARE_API_TOKEN: $
        run: |
          wrangler r2 object put tiles/output.pmtiles \
            --file=dist/output.pmtiles \
            --content-type="application/octet-stream" \
            --cache-control="public, max-age=3600"

Pin the Tippecanoe image tag rather than using latest in production — version drift between the development environment and CI is a frequent source of tile generation inconsistencies.

Pull Request Gating

What a tile build should refuse to merge onFive gates a pull request runs before a tileset change is allowed to merge: tile size, feature drop rate, layer names, zoom range and archive readability.PR GATESNo tile exceeds 500 KBtippecanoe reports the worst tile; fail above the budgetFeature drop rate within tolerancea sudden jump means the source data changed shape, not that the flagsneed tuningLayer names unchanged from the previous builda renamed layer breaks every style rule referencing itZoom range matches the stylegenerating past the style's maxzoom is waste; short of it is missingdetailArchive opens and a sample tile decodesa truncated upload passes every other check
Each of these has already shipped as a production incident somewhere. A gate is cheaper than the rollback it replaces.

For teams treating spatial data as code, tile validation on every PR prevents broken tilesets from merging. A lightweight gating job runs a test subset:

bash
#!/usr/bin/env bash
# ci/validate_tiles.sh — run on every PR
set -euo pipefail

SAMPLE_INPUT="data/sample_1000_features.geojson"
TEST_OUTPUT="/tmp/pr_test.pmtiles"

tippecanoe \
  --output="$TEST_OUTPUT" \
  --layer=test \
  --minimum-zoom=0 \
  --maximum-zoom=14 \
  --include=name \
  --force \
  "$SAMPLE_INPUT"

# Check max tile size (requires pmtiles CLI)
pmtiles verify "$TEST_OUTPUT"

MAX_TILE=$(pmtiles show "$TEST_OUTPUT" | grep -oP 'max_tile_size: \K[0-9]+')
if [ "$MAX_TILE" -gt 500000 ]; then
  echo "ERROR: max tile size ${MAX_TILE} bytes exceeds 500 KB limit"
  exit 1
fi

echo "PASS: max tile size ${MAX_TILE} bytes"

What Makes a Tile Build Automatable

The difference between a build script and an automated pipeline is not the scheduler. It is four properties, each of which has to be designed in rather than added later.

Determinism. The same inputs must produce the same tiles, byte for byte. That means pinning the Tippecanoe version, sorting the input deterministically, and keeping timestamps out of the tile metadata. Without it, every scheduled run publishes a “new” tileset that is identical to the last one, and the versioned-URL scheme downstream warms a cold cache nightly for no reason.

Idempotency. Rerunning the pipeline with unchanged inputs must be a no-op. The mechanism is content hashing: each stage’s output is addressed by a hash of its inputs, so an unchanged extract skips the tiling step entirely. This is what makes a retry safe and a partial failure cheap to resume.

A gate that can fail. An automated build without a verification step is an automated way to publish broken tiles. The gate has to run against the built archive — not against the source data, and not against the logs — and it has to be able to stop the publish step. It is also worth deliberately feeding it a broken archive once, because a gate that has never rejected anything is an assumption rather than a check.

Reversibility. Publishing must be additive: new tiles at a new address, with the previous version still readable. Then a bad build is undone by pointing the style back, not by rebuilding under pressure.

A pipeline with all four is boring to operate, which is the goal. A pipeline missing any one of them will eventually require someone to intervene at an inconvenient hour, and which one is missing determines what that intervention looks like.

Choosing the Input Shape

Tippecanoe reads GeoJSON, newline-delimited GeoJSON, CSV and a few other formats, but the practical decision is narrower: does the pipeline materialise an intermediate file, or stream?

Materialising is simpler to reason about and easier to debug, because the intermediate is inspectable. It costs disk proportional to the source — often several times the size of the final archive — and it costs the wall-clock of writing and reading it. For datasets under a few gigabytes this is entirely reasonable and the simplicity is worth it.

Streaming is the only option once the source outgrows the build machine’s disk, and it changes the failure modes. The producer and Tippecanoe now fail independently, and a producer that dies mid-stream leaves Tippecanoe with a valid archive of partial data — structurally sound, silently incomplete. Any streaming pipeline therefore needs to check the producer’s exit status as well as Tippecanoe’s, and to compare the feature count in the output against what the source promised. That comparison is the check that distinguishes a complete build from a truncated one, and nothing else in the stack will do it for you.

Failure Modes and Debugging

Tile Size Overflow

Symptom: Tile z/x/y is too large: NNN bytes or silent feature dropping at unexpected zoom levels.

Diagnosis:

bash
pmtiles show output.pmtiles | grep max_tile
# or for mbtiles:
sqlite3 output.mbtiles \
  "SELECT zoom_level, MAX(LENGTH(tile_data)) as max_size \
   FROM tiles GROUP BY zoom_level ORDER BY zoom_level;"

Fix: Add --drop-densest-as-needed to allow Tippecanoe to reduce density at overflowing zooms. For datasets where dropping is unacceptable, tighten the --include list first — removing a single high-cardinality text attribute (street addresses, parcel descriptions) often cuts tile sizes by 30–50%.

Projection Mismatch

Symptom: Tiles generate but map renders features in the ocean or at coordinates (0,0).

Diagnosis: Tippecanoe expects EPSG:4326 (WGS 84 decimal degrees) input. Confirm the source CRS:

bash
ogrinfo -al -so data/input.geojson | grep -i 'coordinate system\|proj\|epsg'

Fix: Reproject before passing to Tippecanoe:

bash
ogr2ogr \
  -f GeoJSON \
  -t_srs EPSG:4326 \
  data/reprojected.geojson \
  data/input_3857.geojson

Memory Exhaustion on Large Datasets

Symptom: Killed with no error message, or std::bad_alloc during the sort phase.

Diagnosis: Check peak RSS during a test run:

bash
/usr/bin/time -v tippecanoe --output=/tmp/test.pmtiles --layer=test \
  --maximum-zoom=8 data/large.geojson 2>&1 | grep "Maximum resident"

Fix options:

  1. Pre-filter to a tighter bounding box with ogr2ogr -spat
  2. Split input by geographic region and pass multiple files
  3. Stream GeoParquet via DuckDB (Pattern 3 above) instead of loading full GeoJSON
  4. Reduce --maximum-zoom by 1–2 levels to cut the sort space

Corrupt or Empty Output

Symptom: PMTiles file writes but MapLibre reports source data loading error.

Diagnosis:

bash
pmtiles verify output.pmtiles
pmtiles show output.pmtiles  # Check tile count > 0

A tile count of 0 usually means all features were dropped due to --maximum-zoom set lower than the first zoom where features have enough size to appear. Tippecanoe will not error — it silently writes an empty archive. Set --minimum-zoom=0 explicitly and inspect the feature count at z0.

Layer Name Mismatch

Symptom: Tiles load, no features render, no console errors.

Diagnosis: The --layer flag in Tippecanoe must exactly match source-layer in the MapLibre GL style. Confirm:

bash
pmtiles show output.pmtiles | grep -i layer

Compare against the style JSON layers[].source-layer value. This is the most common silent failure in multi-team workflows where data engineers name layers differently from front-end developers.

FAQ

What makes a tile build automatable rather than merely scripted?

Four properties: determinism, idempotency, a gate that can fail the build, and reversible publishing. A pipeline missing any one will eventually need someone to intervene manually.

Should the pipeline materialise an intermediate file?

Below a few gigabytes, yes — it is simpler and debuggable. Above that, stream, and check the producer’s exit status as well as Tippecanoe’s, because a truncated stream produces a valid archive of partial data.

How do I know a build produced what I asked for?

Decode one tile. tippecanoe-decode on the largest tile at a mid zoom shows the real layer names, the surviving attributes and the actual feature count, none of which the build log reports.

What belongs in the version hash?

Everything that changes the tile bytes: the source snapshot, the flag set, the tool version and the layer schema. Anything omitted is a way to publish a stale tileset under a fresh-looking prefix.

Topic Index

This section covers the full pipeline, with each topic deepening a specific stage:

Tippecanoe CLI Fundamentals — exhaustive flag reference organized by pipeline stage: input, generalization, zoom control, output format, and performance flags. The essential flags for production builds distills the minimum viable flag set for shipping a first tileset.

GeoParquet Input Processing — cloud-native ingestion patterns using DuckDB and pyarrow. Converting large GeoParquet files to vector tiles covers streaming, spatial filtering, and column pushdown for datasets exceeding available RAM.

Geometry Simplification Algorithms — how Tippecanoe’s simplification works under the hood, when to pre-simplify in PostGIS instead, and a direct comparison of Visvalingam-Whyatt vs Douglas-Peucker for tile generation.

Attribute Filtering Rules — strategies for reducing tile size through disciplined attribute management. Dropping unused attributes to reduce tile size benchmarks the size impact of common attribute types.

CI/CD Tile Build Automation — turning a one-off command into a reproducible, gated pipeline: pinned container builds, source-hashed Makefile targets, a tile-size gate, and automating Tippecanoe builds with GitHub Actions.


Related

Next reading Attribute Filtering Rules for Vector Tile Pipelines Next reading CI/CD Tile Build Automation Next reading Geometry Simplification Algorithms for Vector Tile Pipelines Next reading GeoParquet Input Processing for Vector Tile Pipelines Next reading Incremental and Partitioned Tile Builds Next reading Layer Composition and tile-join Next reading Tippecanoe CLI Fundamentals