---
title: A Practical Guide to Valhalla Routing Engine: Tile Pipelines, Custom Costing Models, Map Matching, and Turn-by-Turn Navigation with OpenStreetMap
publishedAt: 2026-08-31
summary: Learn how to build, deploy, and query the open-source Valhalla routing engine with OpenStreetMap data, dynamic costing models, GPS map matching, and mobile-ready turn-by-turn navigation.
---

# A Practical Guide to Valhalla Routing Engine: Tile Pipelines, Custom Costing Models, Map Matching, and Turn-by-Turn Navigation with OpenStreetMap

Valhalla is an open-source, tile-based routing engine designed for high-performance point-to-point navigation, map matching, and matrix calculations using OpenStreetMap (OSM) data. Unlike traditional monolithic routing engines, Valhalla organizes its routing graph into hierarchical spatial tiles loaded on demand or memory-mapped, enabling dynamic runtime costing adjustments without rebuilding precomputed graphs. This guide walks you through generating routing tiles, configuring custom costing options, snapping raw GPS traces with Meili map matching, and parsing turn-by-turn maneuvers for production mobile and web applications.

---

## What Problem Does Valhalla Solve?

Traditional routing engines often require rigid precomputations (such as Contraction Hierarchies) that lock routing weights at build time, forcing expensive re-indexing when vehicle rules or preferences change. Valhalla resolves this by organizing graph data into hierarchical geographical tiles and evaluating dynamic costing at query time, allowing flexible multi-modal routing across cars, trucks, bicycles, and pedestrians. This architecture gives backend and mobile engineers full control over real-time routing penalties, live GPS trace snapping, and granular turn-by-turn narrative generation without running separate server clusters for every vehicle profile.

Routing engines in production mobile apps and microservice architectures face three major challenges:

1. **Static Graph Rigidity**: In engines relying solely on strict Contraction Hierarchies, changing an avoidance parameter (such as penalizing unpaved roads, tolls, or low-clearance bridges) requires querying a separate precalculated profile or running a fallback algorithm. Valhalla uses bidirectional A* search combined with dynamic costing algorithms, allowing clients to modify routing penalties in every HTTP request.
2. **Memory Footprint**: Monolithic routing graphs must be loaded entirely into RAM. Valhalla stores the road network in discrete spatial tiles grouped by administrative hierarchy (Level 0 for highways, Level 1 for arterials, Level 2 for local roads). Servers can use memory-mapped files (`mmap`), drastically lowering idle memory overhead.
3. **Integrated Map Matching (Meili)**: Raw GPS traces from mobile devices suffer from multi-path reflections, sensor drift, and intermittent signal loss. Valhalla includes a built-in Hidden Markov Model (HMM) map-matching engine called Meili that snaps coordinate sequences directly to the underlying OSM way network.

To explore the open-source code and official specs, visit the [Valhalla GitHub Repository](https://github.com/valhalla/valhalla) and the [Valhalla Documentation](https://valhalla.github.io/valhalla/).

---

## Step 1: Installation and Tile Pipeline Setup

Setting up a production-ready Valhalla instance requires downloading OpenStreetMap PBF extracts, configuring tile hierarchy parameters, and building the spatial routing tiles using Docker. The tile generation pipeline converts raw OSM nodes and ways into optimized graph tiles structured across spatial levels, accompanied by optional elevation data. Using containerized tooling ensures reproducible tile builds across development, staging, and production environments.

### 1.1 Preparing the Workspace and Extract

Download your target region in `.osm.pbf` format from [OpenStreetMap Data](https://www.openstreetmap.org/) providers like Geofabrik. Create a dedicated directory structure for data, configuration, and generated tiles:

```bash
mkdir -p valhalla_workspace/{custom_files,valhalla_tiles}
cd valhalla_workspace

# Download a regional OSM PBF extract (e.g., Liechtenstein/Monaco for quick testing or a regional extract)
curl -L -o custom_files/region.osm.pbf https://download.geofabrik.de/europe/liechtenstein-latest.osm.pbf
```

### 1.2 Generating Configuration and Building Routing Tiles

The simplest and most maintainable way to run the tile pipeline is through the official Valhalla Docker container:

```bash
# Generate the base valhalla.json configuration file
docker run --rm -v "$(pwd)/custom_files:/custom_files" \
  ghcr.io/gis-ops/docker-valhalla/valhalla:latest \
  valhalla_build_config \
  --tile-extract /custom_files/valhalla_tiles.tar \
  --tile-dir /custom_files/valhalla_tiles \
  --conf /custom_files/valhalla.json

# Build routing tiles from the PBF extract
docker run --rm \
  -v "$(pwd)/custom_files:/custom_files" \
  ghcr.io/gis-ops/docker-valhalla/valhalla:latest \
  valhalla_build_tiles \
  -c /custom_files/valhalla.json \
  /custom_files/region.osm.pbf

# Package the tiles into a consolidated tar archive for fast server loading
docker run --rm \
  -v "$(pwd)/custom_files:/custom_files" \
  ghcr.io/gis-ops/docker-valhalla/valhalla:latest \
  valhalla_build_extract \
  -c /custom_files/valhalla.json \
  -v
```

### 1.3 Deploying the Valhalla HTTP Service

Create a lightweight `docker-compose.yml` to run the Valhalla routing service as a persistent background daemon:

```yaml
version: '3.8'

services:
  valhalla:
    image: ghcr.io/gis-ops/docker-valhalla/valhalla:latest
    container_name: valhalla_service
    restart: unless-stopped
    ports:
      - "8002:8002"
    volumes:
      - ./custom_files:/custom_files
    environment:
      - serve_tiles=True
      - use_tiles_ignore_pbf=True
      - force_rebuild=False
```

Start the container and verify service health:

```bash
docker compose up -d
curl http://localhost:8002/status | jq .
```

A healthy response returns version metadata and confirmation that tile hierarchies are loaded.

---

## Step 2: Practical Implementation and Production Code Snippets

Querying Valhalla involves sending structured JSON payloads to dedicated HTTP endpoints for routing, map matching, isochrones, and distance matrices. Valhalla exposes dynamic costing models that can be fine-tuned per query without restarting the daemon, giving client applications granular control over route penalties, vehicle dimensions, and maneuver instructions. The following production examples demonstrate practical request payloads and client-side integration patterns.

### 2.1 Turn-by-Turn Routing with Dynamic Costing

The `/route` endpoint calculates optimal paths between two or more waypoints. The `costing_options` object allows real-time penalty overrides:

```bash
curl -X POST http://localhost:8002/route \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [
      {"lat": 47.1410, "lon": 9.5210, "type": "break"},
      {"lat": 47.1650, "lon": 9.5130, "type": "break"}
    ],
    "costing": "auto",
    "costing_options": {
      "auto": {
        "maneuver_penalty": 15,
        "toll_booth_penalty": 30.0,
        "use_highways": 0.8,
        "use_tolls": 0.2,
        "country_crossing_penalty": 300.0
      }
    },
    "directions_options": {
      "units": "kilometers",
      "language": "en-US"
    }
  }' | jq .
```

#### Key Costing Parameters Reference

| Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `use_highways` | Float (0.0–1.0) | `0.5` | Likelihood of favoring highways over local networks. |
| `use_tolls` | Float (0.0–1.0) | `0.5` | Likelihood of routing through toll roads. |
| `maneuver_penalty` | Integer (seconds) | `5` | Penalty added when a maneuver (e.g., turn) occurs. |
| `toll_booth_penalty` | Float (seconds) | `0.0` | Cost in seconds added at toll barrier locations. |
| `width` / `height` / `weight` | Float (meters/tons) | `None` | Restricts routing for commercial vehicles or trucks. |

---

### 2.2 Snapping Noisy GPS Traces with Map Matching (Meili)

When handling raw GPS breadcrumbs collected from mobile devices, coordinate inaccuracies cause naive polylines to cut across buildings or snap to wrong parallel roads. The `/trace_route` endpoint runs the Meili HMM algorithm to project coordinates onto valid OSM segments:

```bash
curl -X POST http://localhost:8002/trace_route \
  -H "Content-Type: application/json" \
  -d '{
    "shape": [
      {"lat": 47.1412, "lon": 9.5211, "time": 1700000000},
      {"lat": 47.1425, "lon": 9.5208, "time": 1700000015},
      {"lat": 47.1448, "lon": 9.5199, "time": 1700000030},
      {"lat": 47.1480, "lon": 9.5185, "time": 1700000050}
    ],
    "costing": "auto",
    "shape_match": "map_snap",
    "trace_options": {
      "search_radius": 25.0,
      "gps_accuracy": 5.0
    }
  }' | jq .
```

* `search_radius`: Maximum distance (in meters) to look for candidate road edges around each point.
* `gps_accuracy`: Standard deviation of GPS measurements (in meters), weighting how strictly candidate paths adhere to raw points.

---

### 2.3 Mobile-Ready Client Implementation (TypeScript / React Native)

Client applications need to parse Valhalla's maneuver list, decode its polyline shape (Valhalla uses encoded polyline with precision 6 by default), and render live turn-by-turn guidance.

Here is a practical, production-ready TypeScript service:

```typescript
// types/valhalla.ts
export interface ValhallaLocation {
  lat: number;
  lon: number;
  type?: 'break' | 'through' | 'via';
}

export interface Maneuver {
  type: number;
  instruction: string;
  verbal_pre_transition_instruction?: string;
  street_names?: string[];
  time: number;
  length: number;
  begin_shape_index: number;
  end_shape_index: number;
}

export interface ValhallaRouteResponse {
  trip: {
    status: number;
    units: string;
    summary: {
      time: number;
      length: number;
    };
    legs: Array<{
      shape: string;
      maneuvers: Maneuver[];
      summary: {
        time: number;
        length: number;
      };
    }>;
  };
}

// services/navigationService.ts
export class ValhallaClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  /**
   * Fetches an optimized route between coordinates
   */
  async getRoute(
    origin: ValhallaLocation,
    destination: ValhallaLocation,
    costing: 'auto' | 'bicycle' | 'pedestrian' = 'auto'
  ): Promise<{ coordinates: [number, number][]; maneuvers: Maneuver[]; totalDurationSec: number }> {
    const payload = {
      locations: [
        { lat: origin.lat, lon: origin.lon, type: 'break' },
        { lat: destination.lat, lon: destination.lon, type: 'break' },
      ],
      costing,
      directions_options: {
        units: 'kilometers',
        language: 'en-US',
      },
    };

    const response = await fetch(`${this.baseUrl}/route`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Valhalla route request failed with status ${response.status}`);
    }

    const data: ValhallaRouteResponse = await response.json();
    const primaryLeg = data.trip.legs[0];

    // Decode polyline with precision 6
    const coordinates = this.decodePolyline6(primaryLeg.shape);

    return {
      coordinates,
      maneuvers: primaryLeg.maneuvers,
      totalDurationSec: data.trip.summary.time,
    };
  }

  /**
   * Decodes Valhalla 6-digit precision polyline strings
   */
  private decodePolyline6(encoded: string): [number, number][] {
    const points: [number, number][] = [];
    let index = 0;
    let lat = 0;
    let lon = 0;

    while (index < encoded.length) {
      let b: number;
      let shift = 0;
      let result = 0;

      do {
        b = encoded.charCodeAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);

      const dlat = (result & 1) !== 0 ? ~(result >> 1) : result >> 1;
      lat += dlat;

      shift = 0;
      result = 0;

      do {
        b = encoded.charCodeAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);

      const dlon = (result & 1) !== 0 ? ~(result >> 1) : result >> 1;
      lon += dlon;

      points.push([lat / 1e6, lon / 1e6]);
    }

    return points;
  }
}
```

---

### 2.4 Isochrones and Many-to-Many Distance Matrix

For dispatching, delivery time estimation, and catchment analysis, Valhalla provides specialized `/isochrone` and `/sources_to_targets` endpoints.

#### Generating Reachability Isochrones

```bash
curl -X POST http://localhost:8002/isochrone \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [{"lat": 47.1410, "lon": 9.5210}],
    "costing": "auto",
    "contours": [
      {"time": 5, "color": "00ff00"},
      {"time": 10, "color": "ffff00"},
      {"time": 15, "color": "ff0000"}
    ],
    "polygons": true
  }' | jq .
```

#### Calculating Distance & Time Matrix

```bash
curl -X POST http://localhost:8002/sources_to_targets \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      {"lat": 47.1410, "lon": 9.5210}
    ],
    "targets": [
      {"lat": 47.1650, "lon": 9.5130},
      {"lat": 47.1350, "lon": 9.5350}
    ],
    "costing": "auto"
  }' | jq .
```

The response provides a flattened array of computed distances (meters) and times (seconds) across all source-target pairs without calculating full geometry shapes, saving bandwidth and compute cycles.

---

## Step 3: Common Pitfalls and Operational Best Practices

Running Valhalla in production requires managing memory during tile generation, handling multi-region extracts cleanly, and optimizing client query payloads. Overlooking tile hierarchy configurations or sending unindexed GPS points can cause query timeouts and excessive CPU usage. Adhering to established operational practices ensures high availability and predictable latency under load.

### 1. Handling Memory Spikes During `valhalla_build_tiles`
Building tiles for entire continents or high-density countries requires significant RAM during the initial node sorting phase. If your build crashes due to out-of-memory (OOM) errors:
* Clip your PBF to the exact bounding box using `osmium-tool` before ingestion.
* Increase swap space on the build host or build on high-memory worker instances, then package the resulting `valhalla_tiles.tar` for distribution to low-memory runtime nodes.

### 2. Matching Polyline Precision
A frequent bug in mobile map rendering is using standard Google Polyline decoder logic (precision 5, `1e5`) instead of Valhalla’s default precision 6 (`1e6`). If your route appears in the ocean or off by a factor of 10, verify your decoder applies division by `1e6` or set `"shape_format": "polyline5"` in your `directions_options`.

### 3. Tuning Map Matching Search Radii
Setting `search_radius` too high (e.g., `> 100m`) forces Meili to evaluate too many candidate edges across parallel avenues, leading to route oscillation. Keep `search_radius` between `15m` and `35m` for city driving, and ensure timestamps are strictly monotonically increasing.

### 4. Tile Storage: Directory vs. Single TAR Extract
For production deployments, package tiles into a consolidated `valhalla_tiles.tar` archive using `valhalla_build_extract`. Storing millions of individual tiny tile files on cloud block storage (like AWS EBS or GCP Persistent Disks) exhausts filesystem inodes and slows container cold-starts.

---

## Frequently Asked Questions

### How do I reduce RAM usage when generating Valhalla tiles for large regions?
Pre-filter the raw OSM PBF file using tools like `osmium tags-filter` to strip out unnecessary non-routable metadata (such as building footprints, landuse polygons, and leisure tags) prior to running `valhalla_build_tiles`. Furthermore, run tile generation on an ephemeral high-memory cloud instance, bundle the output into a single `valhalla_tiles.tar` using `valhalla_build_extract`, and deploy the archived file to runtime instances using memory-mapped (`mmap`) storage.

### What is the difference between Valhalla and OSRM for mobile navigation applications?
OSRM uses Contraction Hierarchies (CH) by default, which yields fast static routing responses but cannot modify vehicle constraints (like weight limits, dynamic toll avoidance, or live speed penalties) at query time without maintaining multiple precalculated graphs. Valhalla uses a tile-based hierarchy with dynamic runtime costing, native multi-modal profiles, built-in Meili HMM map matching, and richer turn-by-turn narrative localization out of the box.

### How do I handle map matching for noisy mobile GPS traces using Valhalla?
Submit coordinate sequences with monotonic UTC timestamps to the `/trace_route` or `/trace_attributes` endpoint using `shape_match: "map_snap"`. Calibrate the `gps_accuracy` parameter (typically 5–10 meters for standard mobile phone chipsets) and keep `search_radius` tightly bound between 20 and 40 meters to prevent the Viterbi solver from jumping to adjacent service roads or overpasses.

---

### About the Author
**Furkan Çetinkaya** is a Mobile-focused Software Developer specializing in React Native, native bridge integrations (Kotlin & Swift), and supporting backend services. Experienced in maintaining high-impact mobile applications and developer SDKs.
- [GitHub](https://github.com/cetfu)
- [LinkedIn](https://www.linkedin.com/in/cetfu)