Valhalla will compute an isochrone for you, against the same tiles and costing model that serve its routes. For service-area work that is usually the right choice: the polygon is consistent with the routes the same engine returns, and the whole computation happens in one request. This page covers using the endpoint well, as part of Valhalla configuration for multi-modal analysis within Python Routing Engines & Isochrone Mapping.
The parameters that matter are not the obvious ones. Contour definition is straightforward; denoise and generalize quietly determine whether the result is usable, and the difference between polygons and lines determines whether it can be used for area work at all.
When to use this approach
Prefer the engine endpoint when the isochrone needs to agree with the engine’s routes — service-area definition, coverage reporting, anything a customer will compare against a quoted travel time. Consistency between the two is worth more than control over the polygonisation.
Prefer a local pipeline, per generating isochrones with PySAL and GeoPandas, when the polygonisation itself is the subject — comparing methods, applying a custom cost surface, or needing the reachable node set rather than only its outline.
Implementation
The request is a single JSON body, and the shape of it is where most integration errors live.
# requires: httpx (pip install httpx)
import httpx
VALHALLA = "http://valhalla:8002"
def service_area(lon: float, lat: float, minutes: list[int],
*, costing: str = "truck", denoise: float = 0.4,
generalize: float = 50.0) -> dict:
"""Nested service-area polygons for one origin."""
body = {
"locations": [{"lon": lon, "lat": lat}],
"costing": costing,
# Time OR distance — never both in one request
"contours": [{"time": m} for m in sorted(minutes)],
"polygons": True, # False returns lines, useless for area work
"denoise": denoise, # drop fragments below this share of the largest
"generalize": generalize, # Douglas-Peucker tolerance in metres
"show_locations": True,
}
r = httpx.post(f"{VALHALLA}/isochrone", json=body, timeout=60.0)
r.raise_for_status()
return r.json()
polygons: True is the setting people miss. The default returns LineStrings, which render acceptably and cannot be used for any area or containment operation — a spatial join against them silently matches nothing, which looks like an empty result rather than an error.
Sorting the contour list is a small defence. Valhalla returns features in its own order and mixing unsorted inputs with an assumption about output order is a common source of mislabelled bands.
The response is a GeoJSON FeatureCollection whose features are ordered outermost first, which is the opposite of what most consumers assume:
# requires: geopandas, shapely (pip install geopandas shapely)
import geopandas as gpd
from shapely.geometry import shape
def to_frame(response: dict, crs_metric: str = "EPSG:3035") -> gpd.GeoDataFrame:
"""Parse the FeatureCollection into a metric frame with true band areas."""
rows = []
for feat in response["features"]:
props = feat.get("properties", {})
if "contour" not in props:
continue # skip the location marker feature
rows.append({"minutes": float(props["contour"]),
"geometry": shape(feat["geometry"])})
gdf = gpd.GeoDataFrame(rows, crs="EPSG:4326").to_crs(crs_metric)
gdf = gdf.sort_values("minutes").reset_index(drop=True)
# Contours are nested, so a band is a ring rather than the whole polygon
inner = gdf.geometry.shift(1)
gdf["band_geometry"] = [
g if prev is None else g.difference(prev)
for g, prev in zip(gdf.geometry, inner)
]
gdf["band_area_km2"] = (gdf["band_geometry"].area / 1e6).round(3)
return gdf
The nesting is the detail that produces wrong numbers when missed. A 5, 10 and 15 minute request returns three polygons where each contains the smaller ones, so summing their areas triple-counts the innermost. Differencing successive contours gives the ring that actually represents “reachable between 10 and 15 minutes”.
Reprojecting before computing area is the usual requirement — a .area call on EPSG:4326 geometry returns square degrees, which vary with latitude and mean nothing.
It is worth being deliberate about the denoise and generalize parameters rather than accepting whatever the defaults produce, because they trade fidelity for vertex count and the right trade differs by use. A polygon destined for a web map benefits from aggressive generalization: the viewer cannot resolve ten-metre detail and the payload shrinks by an order of magnitude. A polygon destined for zonal statistics does not, because generalization moves the boundary across whichever population cells happen to sit near it, and the resulting area error is systematic rather than random. Requesting the geometry twice with different settings costs one extra call and avoids the temptation to reuse a display polygon for analysis.
Equally, treat the isochrone as a snapshot of one departure rather than a property of the location. Two requests from the same origin an hour apart will differ, sometimes substantially, and for transit costing they will differ enormously. Storing the departure timestamp alongside the geometry is what makes a later comparison meaningful.
Key parameters and tuning
| Parameter | Recommended value | Notes |
|---|---|---|
polygons |
True |
The default returns lines, which cannot be used for area or containment |
contours |
time or distance, not both | Mixing them returns an error |
denoise |
0.3–0.5 | Below 0.2 the output speckles; above 0.6 real satellite regions are dropped |
generalize |
50 m street scale, 500 m regional | Set from the drawing scale, not from file size |
costing |
match the fleet | A truck service area computed with auto costing overstates reach |
date_time |
set for transit | Without it a multimodal isochrone uses an unspecified departure |
| Area CRS | equal-area projection | Degrees are not an area unit |
Integration points
Against a local pipeline. Comparing Valhalla’s polygon with one built locally from the same origin is a useful cross-check on both, but the two will differ by several percent for structural reasons covered in comparing alpha shapes and concave hulls. Treat a large divergence as a signal and a small one as expected.
Into accessibility scoring. The band geometries feed zonal statistics the same way a locally built polygon does — see population-weighted accessibility scoring. Using the differenced bands rather than the nested contours is what keeps the population counts additive.
With multimodal costing. For transit isochrones the departure time is as consequential as it is for routing, because service frequency dominates reach. The parameters are the same ones covered in Valhalla configuration for multi-modal analysis, and omitting date_time produces an isochrone for an unspecified moment.
Validation checklist
- Bands sum to the outer contour. The differenced areas must total the largest polygon’s area to within rounding.
- The response contains polygons, not lines. Assert the geometry type. A LineString result means
polygonswas left at its default. - Areas are computed in a projected CRS. Assert the frame’s CRS is projected before any
.areacall. - The origin falls inside the smallest contour. A trivial containment check that catches transposed coordinates immediately.
- Denoise is not dropping real regions. Compare feature counts at your setting and at zero. A large difference means genuine disconnected service areas — an island, a valley — may be being discarded.
Related
- Valhalla configuration for multi-modal analysis — the costing and tile setup this endpoint shares with routing
- Generating isochrones with PySAL and GeoPandas — the local alternative, and when control over polygonisation matters
- Comparing alpha shapes and concave hulls — why engine and local polygons legitimately differ
- Valhalla cost matrix generation for urban planners — the matrix alternative when discrete origins beat a continuous surface