Running Planetiler on a Regional Extract Jump to heading
Get a complete tile archive out of a country-sized extract in one run, on hardware you actually have, by sizing the two storage decisions before you start rather than discovering them at hour four.
Prerequisites Jump to heading
Conceptual minimum Jump to heading
A Planetiler run has two storage decisions and everything else follows from them.
Node location storage. Building way geometry needs every referenced node’s coordinate. Planetiler can hold that map on the Java heap, in off-heap memory-mapped files, or in a direct memory-mapped array indexed by node identifier. The array is the fastest and is sized by the highest node identifier in the planet, not by your region — which is why a small country extract still wants tens of gigabytes of address space. Memory mapping makes that workable: the operating system pages in what is touched.
The temporary feature store. Every feature the profile emits is written to a temporary store, sorted spatially, then read back per tile. This is sequential-write then sequential-read, so it wants throughput rather than low latency, and it wants room — typically a few times the extract size.
Neither decision is about the tile output. Both are about whether the build completes.
Runnable solution Jump to heading
#!/usr/bin/env bash
# Build a regional tile archive with Planetiler.
set -euo pipefail
EXTRACT="${1:?usage: build.sh <extract.osm.pbf>}"
AREA="$(basename "$EXTRACT" .osm.pbf)"
TMP="/fast-local/planetiler-tmp" # MUST be local disk, not network
OUT="${AREA}.pmtiles"
MAXZOOM=14
mkdir -p "$TMP"
FREE_KB="$(df -Pk "$TMP" | awk 'NR==2 {print $4}')"
EXTRACT_KB="$(du -k "$EXTRACT" | cut -f1)"
# The temporary store plus the node store want several times the extract.
if [ "$FREE_KB" -lt $(( EXTRACT_KB * 6 )) ]; then
echo "need ~$(( EXTRACT_KB * 6 / 1024 / 1024 )) GiB free on $TMP, have $(( FREE_KB / 1024 / 1024 ))" >&2
exit 1
fi
java -Xmx8g \
-jar planetiler.jar \
--osm-path="$EXTRACT" \
--output="$OUT" \
--maxzoom="$MAXZOOM" \
--tmpdir="$TMP" \
--nodemap-type=array \
--nodemap-storage=mmap \
--force \
2>&1 | tee "build-${AREA}.log"
# The log records per-phase timings; keep it beside the archive.
grep -E "^\s*(read|sort|render|write)" "build-${AREA}.log" || true
from __future__ import annotations
import logging
import re
import sys
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("osm.planetiler.log")
# Planetiler prints a line per phase with an elapsed time, e.g. " read: 12m34s".
PHASE_RE = re.compile(r"^\s*(\w+)\s*:\s*(?:(\d+)m)?(\d+(?:\.\d+)?)s", re.M)
def parse_phases(log: str) -> dict[str, float]:
phases: dict[str, float] = {}
for name, minutes, seconds in PHASE_RE.findall(log):
phases[name] = float(minutes or 0) * 60 + float(seconds)
return phases
def diagnose(log_path: Path) -> str:
phases = parse_phases(log_path.read_text(encoding="utf-8"))
if not phases:
return "no phase timings found — did the build finish?"
total = sum(phases.values())
for name, seconds in sorted(phases.items(), key=lambda kv: -kv[1]):
logger.info("%-8s %6.0fs %4.1f%%", name, seconds, 100 * seconds / total)
worst = max(phases, key=phases.get)
share = phases[worst] / total
if worst == "read" and share > 0.55:
return "READ dominates: the profile is doing too much work per element"
if worst == "sort" and share > 0.45:
return "SORT dominates: give the temporary store faster disk or more memory"
if worst == "render" and share > 0.45:
return "RENDER dominates: lower the maximum zoom or add cores"
return f"balanced; {worst} is largest at {share:.0%}"
if __name__ == "__main__":
logger.info("verdict: %s", diagnose(Path(sys.argv[1])))
Step-by-step walkthrough Jump to heading
- Check the disk before launching. The script refuses to start when free space is under roughly six times the extract, because failing at the start costs seconds and failing at hour four costs the whole run.
- Keep the temporary directory local. The feature store is written and read at high throughput; a network volume turns a two-hour build into an overnight one.
- Choose the array node map with memory mapping. The array is indexed directly by node identifier, which is the fastest lookup available, and memory mapping means the address space it reserves is not resident memory.
- Keep the heap modest. Planetiler does most of its heavy storage off-heap, so an enormous heap does not help and takes memory the page cache would use better.
- Cap the maximum zoom deliberately. The render phase scales with tile count, which quadruples per level; this is the single largest lever on that phase.
- Tee the log. Per-phase timings are the only cheap diagnostic, and they are gone if the output is not kept.
- Diagnose from the phase shares. The parser turns the log into a verdict naming the resource to change, rather than a wall of numbers.
Verification Jump to heading
- The archive opens and declares the expected zoom range. Read its metadata before anybody points a client at it.
- A dense tile is non-trivial in size. Fetch a city-centre tile at the maximum zoom; a few kilobytes means the profile matched almost nothing.
- Every expected layer appears. Decode a sample tile and compare the layer set against the profile.
- Phase timings look sane. No phase should be over about three-quarters of the total on a well-provisioned machine.
- The temporary directory is empty afterwards. Left-over files mean the run did not finish cleanly, whatever the exit status said.
Common errors and fixes Jump to heading
| Symptom | Root cause | One-line fix |
|---|---|---|
| Build fails hours in, out of disk | Temporary store under-provisioned | Check for several times the extract size before starting |
| Build far slower than expected | Temporary directory on a network volume | Point it at local disk |
| Read phase dominates | Profile doing per-element lookups | Precompute into an immutable structure before the run |
| Render phase dominates | Maximum zoom set too deep | Lower it and rely on client overzoom |
| Out of memory with a large heap | Heap taking memory the page cache needs | Reduce the heap; storage is mostly off-heap |
| Tiles nearly empty | Profile matches almost nothing | Log per-layer match counts during a short run |
| Archive missing layers | Profile branch never reached | Assert each layer produced features before shipping |
Specification reference Jump to heading
Planetiler builds a vector tile archive from an OSM extract in a single process, storing node locations in a configurable map — on the Java heap, in off-heap memory, or in a memory-mapped array indexed by node identifier — and writing intermediate features to a temporary directory that is sorted and read back during rendering. See the Planetiler documentation for the storage options, their memory implications, and the per-phase progress output.
Frequently Asked Questions Jump to heading
Why does a small country extract need so much address space?
Because the array node map is indexed by node identifier, and identifiers are assigned globally across the whole planet rather than per region. A recently created node in a small country can carry an identifier in the billions, so the array must be large enough to index it. Memory mapping is what makes this practical: the reservation is address space rather than resident memory, and the operating system pages in only the parts actually touched.
Should I give the process a very large heap?
No. Planetiler deliberately keeps its heavy storage off-heap and memory-mapped, so a large heap mostly takes memory away from the page cache that the node map and feature store rely on. A modest heap with plenty of free system memory for caching usually outperforms a configuration that hands most of the machine to the runtime.
How do I tell whether my profile is the bottleneck?
Read the phase timings. Profile cost lands entirely in the read phase, so a read phase taking well over half the total run is a strong signal that the per-element path is doing more than it should. The usual culprits are compiling patterns, allocating collections, or consulting a large lookup structure inside the callback — all of which are multiplied by the element count.
Can I run it against the full planet on one machine?
Yes, and that is what it is designed for, though it wants a substantial machine: a lot of memory, many cores and fast local storage for the temporary feature store. The build is vertical rather than distributed, which in practice is simpler and cheaper than a cluster for something that runs in hours. Size the temporary directory generously; it is the constraint that fails builds latest and most expensively.
Related Jump to heading
- Planetiler & Tilemaker Workflows — the parent topic and the comparison behind this choice.
- Writing a Tilemaker Lua Profile for OSM Tags — the sibling tool with a scripted profile.
- Serving PMTiles from Object Storage — publishing the archive this run produces.
- Sizing PBF Chunk Batches to a Memory Budget — the same storage arithmetic in a parsing pipeline.
- Profiling Peak Memory of an OSM Parser — measuring rather than estimating the node store.
Up one level: Planetiler & Tilemaker Workflows.