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.

Tippecanoe automated generation pipeline Five stages: Ingest, Preprocess, Tippecanoe, Package, Deliver — connected left to right with arrows Ingest .geojson .geoparquet Preprocess ogr2ogr / GDAL schema validate Tippecanoe zoom 0–14 simplify · filter Package .pmtiles .mbtiles Deliver S3 / R2 / GCS CDN cache

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

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

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"

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.

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 Tippecanoe CLI Fundamentals