Problem
The default cng-datasets workflow PMTiles step is a single-pod pipeline:
ogr2ogr -f GeoJSONSeq /tmp/$DATASET.jsonl /vsicurl/s3:.../$DATASET.parquet
tippecanoe -o /tmp/$DATASET.pmtiles -l $DATASET ... /tmp/$DATASET.jsonl
rclone copy ...
This works fine for ~few-million-feature datasets but fails for very large vector
datasets. Concretely, reprocessing NWI (38,065,251 features, 76 GB merged
GeoParquet on S3) hit two failure modes:
-
Default ephemeral storage (50 Gi from the namespace LimitRange) — pod evicted:
ephemeral local storage usage exceeds the total limit of containers 50Gi
The intermediate GeoJSONSeq for 38M features grows past 100+ GB even with
-select dropping bulky columns, because GeoJSON-text representation expands
geometries 2-3x relative to compressed parquet.
-
Raised ephemeral request to 150 Gi (the 50 Gi is a LimitRange default, not a
hard quota, so the pod scheduled). It landed on a node with only 190 GB free of
869 GB total. The jsonl grew ~1 GB/min; the node would have hit disk-pressure
eviction before tippecanoe finished reading.
Workaround we used (per-state + tile-join)
For NWI, source.coop already exposes per-state parquet ({ST}_Wetlands.parquet),
so we sidestepped the monolithic step:
- Indexed Job, 51 completions, parallelism 20. Each pod reads its own raw
per-state parquet directly from S3, runs ogr2ogr -t_srs EPSG:4326 -select ...,
then tippecanoe -l <dataset> -z14 --drop-densest-as-needed ..., uploads to
s3://<bucket>/<dataset>-state-tiles/{ST}.pmtiles.
- Per-pod resources: 4 CPU, 16 Gi RAM, 50 Gi ephemeral — fits on any node.
- Merge job:
tile-join -o $DATASET.pmtiles -f *.pmtiles concatenates all 51
single-layer tiles into one final PMTiles. Tile-join is fast (~5 min for the
13 GiB of per-state tiles).
Each per-state job processed 1.5K (DC) - 1.6M (AK) features in 2-15 min; the whole
51-state batch took ~71 min (AK was the longest pole). Final merge ~5 min.
Proposed enhancement: chunked-PMTiles backend
Add a chunked PMTiles backend to cng-datasets workflow, mirroring the existing
indexed-Job pattern used for hex. Suggested CLI:
cng-datasets workflow \
--dataset nwi \
--source-url s3://bucket/nwi.parquet \
--bucket public-wetlands \
--pmtiles-backend chunked \
--pmtiles-chunk-by state_code # or: h0 | bbox | row_groups
--pmtiles-completions 51 \
--pmtiles-parallelism 20
The generator would emit:
<dataset>-state-tiles.yaml — indexed Job; each pod reads its chunk (filter on
--pmtiles-chunk-by column), runs ogr2ogr → tippecanoe, uploads a sub-PMTiles
to a staging prefix.
<dataset>-pmtiles-merge.yaml — single pod, downloads staged sub-PMTiles, runs
tile-join, uploads the final canonical PMTiles.
Design notes
- Auto-detect when chunking is needed: feature count > threshold (e.g. 5M)
triggers chunked backend automatically; the user can override either way with
--pmtiles-backend monolithic|chunked.
- Layer name consistency: every per-chunk
tippecanoe -l <layer> MUST agree so
tile-join produces a single-layer output (otherwise you get N layers in the
merged PMTiles, which breaks MapLibre styling).
- Default chunking columns:
state_code, country_code, h0, region — any
string/int column with low cardinality and roughly balanced row counts. If none
exists, fall back to row-group chunking using parquet metadata.
- Attribute pruning: when the dataset has many columns, default
-select to a
small whitelist of small string/int attrs. Bulky columns (Shape_Length,
Shape_Area, doubles in general) should be dropped from tiles since
vector-tile rendering doesn't use them — restyling joins back via the FID.
- Per-pod resources: 4 CPU, 16 Gi RAM, 50 Gi ephemeral is a safe default for
chunks up to ~3M features.
Reference numbers (NWI, 38M features)
| Approach |
Wall-clock |
Peak ephemeral / pod |
Outcome |
| Monolithic (50 Gi default) |
~80 min before evict |
>50 Gi |
Failed (evicted) |
| Monolithic (150 Gi req) |
est. ~3 hr |
est. 150+ Gi |
Risk of node disk-pressure eviction; cancelled at 60 Gi jsonl |
| Per-state + tile-join |
71 min (51 pods, |
|
=20) + 5 min merge |
Final PMTiles size: ~13 GiB (sum of per-state) before merge; tile-join output TBD
(probably similar or smaller after dedup).
Files (reference)
For reference, the YAMLs we used are in boettiger-lab/data-workflows:
catalog/wetlands/k8s/nwi-v2/per-state-pmtiles.yaml
catalog/wetlands/k8s/nwi-v2/pmtiles-merge.yaml
These would be a reasonable template for the generator.
cc @boettiger
Problem
The default
cng-datasets workflowPMTiles step is a single-pod pipeline:This works fine for ~few-million-feature datasets but fails for very large vector
datasets. Concretely, reprocessing NWI (38,065,251 features, 76 GB merged
GeoParquet on S3) hit two failure modes:
Default ephemeral storage (50 Gi from the namespace LimitRange) — pod evicted:
The intermediate GeoJSONSeq for 38M features grows past 100+ GB even with
-selectdropping bulky columns, because GeoJSON-text representation expandsgeometries 2-3x relative to compressed parquet.
Raised ephemeral request to 150 Gi (the 50 Gi is a LimitRange default, not a
hard quota, so the pod scheduled). It landed on a node with only 190 GB free of
869 GB total. The jsonl grew ~1 GB/min; the node would have hit disk-pressure
eviction before tippecanoe finished reading.
Workaround we used (per-state + tile-join)
For NWI, source.coop already exposes per-state parquet (
{ST}_Wetlands.parquet),so we sidestepped the monolithic step:
per-state parquet directly from S3, runs
ogr2ogr -t_srs EPSG:4326 -select ...,then
tippecanoe -l <dataset> -z14 --drop-densest-as-needed ..., uploads tos3://<bucket>/<dataset>-state-tiles/{ST}.pmtiles.tile-join -o $DATASET.pmtiles -f *.pmtilesconcatenates all 51single-layer tiles into one final PMTiles. Tile-join is fast (~5 min for the
13 GiB of per-state tiles).
Each per-state job processed 1.5K (DC) - 1.6M (AK) features in 2-15 min; the whole
51-state batch took ~71 min (AK was the longest pole). Final merge ~5 min.
Proposed enhancement: chunked-PMTiles backend
Add a chunked PMTiles backend to
cng-datasets workflow, mirroring the existingindexed-Job pattern used for hex. Suggested CLI:
cng-datasets workflow \ --dataset nwi \ --source-url s3://bucket/nwi.parquet \ --bucket public-wetlands \ --pmtiles-backend chunked \ --pmtiles-chunk-by state_code # or: h0 | bbox | row_groups --pmtiles-completions 51 \ --pmtiles-parallelism 20The generator would emit:
<dataset>-state-tiles.yaml— indexed Job; each pod reads its chunk (filter on--pmtiles-chunk-bycolumn), runsogr2ogr → tippecanoe, uploads a sub-PMTilesto a staging prefix.
<dataset>-pmtiles-merge.yaml— single pod, downloads staged sub-PMTiles, runstile-join, uploads the final canonical PMTiles.Design notes
triggers chunked backend automatically; the user can override either way with
--pmtiles-backend monolithic|chunked.tippecanoe -l <layer>MUST agree sotile-joinproduces a single-layer output (otherwise you get N layers in themerged PMTiles, which breaks MapLibre styling).
state_code,country_code,h0,region— anystring/int column with low cardinality and roughly balanced row counts. If none
exists, fall back to row-group chunking using parquet metadata.
-selectto asmall whitelist of small string/int attrs. Bulky columns (
Shape_Length,Shape_Area, doubles in general) should be dropped from tiles sincevector-tile rendering doesn't use them — restyling joins back via the FID.
chunks up to ~3M features.
Reference numbers (NWI, 38M features)
Final PMTiles size: ~13 GiB (sum of per-state) before merge; tile-join output TBD
(probably similar or smaller after dedup).
Files (reference)
For reference, the YAMLs we used are in
boettiger-lab/data-workflows:catalog/wetlands/k8s/nwi-v2/per-state-pmtiles.yamlcatalog/wetlands/k8s/nwi-v2/pmtiles-merge.yamlThese would be a reasonable template for the generator.
cc @boettiger