Skip to content

Routing with Valhalla

This page explains how Pumperly plans routes and how to run the routing engine it needs. Routing is optional. Without it, the map, prices and station search by area all keep working.

Valhalla is an open-source routing engine built on OpenStreetMap data. It turns a start, an end and optional stops into a road route with a travel time. Pumperly talks to it over HTTP. You run Valhalla yourself, next to the app.

What Pumperly uses Valhalla for

Two API routes call Valhalla. Both send POST /route requests with the auto costing, which means a car. Pumperly has no truck, bicycle or walking profile.

Pumperly endpoint What it asks Valhalla Limits
POST /api/route One route from origin to destination. Without stops, it asks for 2 alternatives, so a visitor sees up to 3 routes. With stops (up to 5), it asks for a single route through them. 30 requests a minute per client. 10-second timeout, 15 seconds when alternatives are requested.
POST /api/route-detour For each station along the route, the time of a trip that passes through the station. The difference from the plain route is the station's detour time. 10 requests a minute per client. Up to 150 stations per request, each with its own Valhalla call and a 10-second timeout.

The detour endpoint is the heavy one. One visitor who moves the detour slider can cause 150 Valhalla calls. Pumperly limits this in two places:

  • Each detour request works through its stations with 8 workers at most.
  • All Valhalla calls in the app share one pool of VALHALLA_MAX_INFLIGHT slots, 6 by default. A call waits in a queue until a slot is free. A visitor who leaves the page cancels the calls still waiting.
sequenceDiagram
    participant B as Browser
    participant A as Pumperly app
    participant V as Valhalla
    B->>A: POST /api/route (origin, destination)
    A->>V: POST /route (auto, alternates: 2)
    V-->>A: main route + alternatives
    A-->>B: routes with geometry and timing
    B->>A: POST /api/route-detour (stations along the route)
    loop each station, at most VALHALLA_MAX_INFLIGHT at once
        A->>V: POST /route (via the station, duration only)
        V-->>A: travel time
        A-->>B: one NDJSON line: station id and detour minutes
    end

The detour answers stream back one line at a time, so the list fills in while Valhalla works. A station whose call fails gets detourMin: -1 and shows no detour time.

The rate limits need a reverse proxy

The per-client limits identify a client by the first address in X-Forwarded-For, then by X-Real-IP. Put Pumperly behind a reverse proxy that sets those headers and replaces any value the client sent. Without one, every visitor with no such header shares a single limit, and a visitor who sends a fake header gets a limit of their own.

Running without Valhalla

Leave VALHALLA_URL unset. Then:

  • POST /api/route answers 502 with {"error": "Routing service unavailable"}, and the route panel shows an error.
  • POST /api/route-detour returns -1 for every station.
  • The map, prices, station popups, clustering and the scrapers work as normal.

Pumperly behaves the same way when Valhalla is set but unreachable, still building its tiles, or has no road near the requested points. Route requests answer 502, with the error Routing service unavailable or Route calculation failed, and detours return -1.

Connecting Pumperly to Valhalla

Variable Default Set it to
VALHALLA_URL unset Valhalla's base URL without a trailing slash, such as http://pumperly-valhalla:8002. Pumperly appends /route.
VALHALLA_MAX_INFLIGHT 6 How many Valhalla calls may run at once. Raise it if Valhalla has spare CPU and detour times arrive slowly. Lower it if Valhalla struggles. It must be a whole number of 1 or more.
PUMPERLY_MAX_DETOUR_STATIONS 150 The most stations one detour request may route. Lower it to reduce load. Extra stations are thinned out evenly along the route.

Never set VALHALLA_MAX_INFLIGHT to 0 or leave it empty

With 0, an empty value or one that is not a number, no call ever gets a slot. Every route request then hangs. Delete the variable to get the default.

Running Valhalla with Docker Compose

The project uses the gis-ops Valhalla image. On its first start it downloads OpenStreetMap extracts, builds routing tiles from them and then serves requests. The tiles are kept in a volume, so later starts skip the build.

A service for Spain only looks like this. Add it to the Compose file that runs the app. Run with Docker Compose covers the rest of that file.

services:
  valhalla:
    image: ghcr.io/gis-ops/docker-valhalla/valhalla:3.5.1
    container_name: pumperly-valhalla
    restart: unless-stopped
    environment:
      tile_urls: https://download.geofabrik.de/europe/spain-latest.osm.pbf
      use_tiles_ignore_pbf: "False"
      serve_tiles: "True"
      build_elevation: "False"
      build_admins: "True"
      build_time_zones: "True"
      build_transit: "False"
      server_threads: "4"
    volumes:
      - pumperly-valhalla:/custom_files
    deploy:
      resources:
        limits:
          memory: 24G

  app:
    environment:
      VALHALLA_URL: http://pumperly-valhalla:8002

volumes:
  pumperly-valhalla:

These settings belong to the gis-ops image, not to Pumperly. The ones above do this:

Setting Value What it does
tile_urls one extract URL The OpenStreetMap extract to download on the first start.
serve_tiles True Start serving routes once the tiles are built.
use_tiles_ignore_pbf False Build the tiles from the extract rather than expecting ready-made tiles.
build_admins True Build country and region borders, which Valhalla uses for rules that differ by country.
build_time_zones True Build time zone data.
build_elevation, build_transit False Pumperly uses neither height data nor public transport.
server_threads 4 Threads that answer requests. Match it to the CPU cores you can spare.

The image's own README lists every setting it accepts.

Pumperly reaches Valhalla on the internal Compose network. Port 8002 does not need to be published on the host.

Choosing the map area

Valhalla can only route where its extract has roads. Cover every country where your visitors plan trips. Geofabrik publishes extracts per continent, country and region.

Set tile_urls to a single extract. A region extract such as europe/dach (Germany, Austria and Switzerland) or a whole continent covers several countries in one file.

tile_urls: https://download.geofabrik.de/europe/portugal-latest.osm.pbf

Merge the extracts into one file first, with osmium:

osmium merge spain-latest.osm.pbf portugal-latest.osm.pbf france-latest.osm.pbf \
  -o merged.osm.pbf

Then either host merged.osm.pbf somewhere the container can download it and put that URL in tile_urls, or copy it into the Valhalla volume and remove tile_urls. The image builds from every .pbf file in /custom_files, so leave only the merged file there.

Give Valhalla one extract, not several

Building from several separate extracts has crashed the tile build with SIGABRT. Merge them first, as shown above.

Countries outside Europe need their own extracts, such as australia-oceania/australia, south-america/argentina, north-america/mexico or asia/taiwan. Merge them in the same way.

Memory, disk and time

The tile build is the expensive part. It happens once, and again whenever you change the extract.

Phase Memory Time
Tile build Grows with the extract. Plan about 24 GB for a large multi-country extract. The Helm chart's default limit is 24 GiB. Grows with the extract. A large multi-country extract takes hours.
Serving About 2 GB Loading tiles after a restart takes up to a minute. See Troubleshooting.

Disk holds the downloaded extract and the built tiles. Both grow with the area. The Helm chart's default volume is 100 GiB.

Build Valhalla and Photon one after the other

Both builds need a lot of memory. On a machine with limited RAM, start Valhalla first and let it finish before you start Photon.

Changing or refreshing the extract

The tiles never update themselves. Roads change, so rebuild from a fresh extract from time to time, for example monthly. Rebuild also whenever you add or remove a country.

To rebuild, stop Valhalla, then delete its volume and start it again:

docker compose stop valhalla
docker compose rm -f valhalla
docker volume rm <project>_pumperly-valhalla
docker compose up -d valhalla

Replace <project> with your Compose project name. docker volume ls shows the full volume name. If you copied a merged extract into the volume yourself, copy the new one in after you recreate it.

Running Valhalla with Helm

The chart can run Valhalla for you. When valhalla.enabled is true, the chart sets VALHALLA_URL to its own Valhalla service on port 8002. To use a Valhalla server you already run, leave it off and set externalServices.valhallaUrl.

values.yaml
valhalla:
  enabled: true
  tileUrls: "https://download.geofabrik.de/europe/spain-latest.osm.pbf"
  serverThreads: "4"
  persistence:
    size: 100Gi
  resources:
    requests:
      memory: 4Gi
    limits:
      memory: 24Gi

Apart from enabled, which defaults to false, the values shown are the chart defaults. The chart passes tileUrls to the image as tile_urls. For several countries, point it at one merged extract. The chart probes Valhalla on /status, so Kubernetes sends it traffic only once it answers. Run on Kubernetes with Helm covers the rest of the chart.

Checking that routing works

First check that the app container can reach Valhalla. The app image includes wget. Replace pumperly with the name of your app container:

docker exec pumperly wget -qO- http://pumperly-valhalla:8002/status

A JSON answer means Valhalla is up. Then ask Pumperly for a route, here from Madrid to Valencia:

curl -s -X POST http://localhost:3000/api/route \
  -H 'Content-Type: application/json' \
  -d '{"origin": [-3.7038, 40.4168], "destination": [-0.3763, 39.4699]}'

Coordinates are [longitude, latitude]. A working setup returns {"routes": [...]} with one to three routes. See HTTP API for the full request and response.

Troubleshooting

Symptom Likely cause What to do
Every route request answers 502 VALHALLA_URL unset or wrong, or Valhalla still building Check the variable, then Valhalla's log. The first build takes a while.
Routes fail for a minute after Valhalla restarts Valhalla is still loading its tiles Wait. Short routes recover first, long ones after 30 to 60 seconds.
Routes fail in one country only That country is not in the extract Add it and rebuild. See Choosing the map area.
The tile build crashes with SIGABRT Several separate extracts Merge them into one file.
The tile build crash-loops with double free or corruption at the same point Malformed level tags on indoor corridor ways in the OpenStreetMap data Drop those ways before building: osmium tags-filter -i merged.osm.pbf w/highway=corridor -o filtered.osm.pbf.
Route requests hang and never answer VALHALLA_MAX_INFLIGHT is 0, empty or not a number Delete it or set a whole number of 1 or more.
Detour times arrive slowly Few slots, or a busy Valhalla Raise VALHALLA_MAX_INFLIGHT if Valhalla has spare CPU, or lower PUMPERLY_MAX_DETOUR_STATIONS.
429 Too many requests The per-client rate limit Behind a proxy that does not set X-Forwarded-For, all visitors share one limit. Fix the proxy headers.

For logs and general checks, see Monitoring and troubleshooting.