Picking a routing engine is a decision you live with for years, not a weekend spike — the graph preprocessing pipeline, deployment topology, and API contract you choose ripple through every downstream automation in Routing API Automation & Fleet Integration. This guide compares OSRM, Valhalla, and GraphHopper on the axes that actually determine production fit: feature coverage, query latency, memory footprint, dynamic weight updates, multimodal support, and freight/HGV routing. It assumes you have already read about deploying OSRM with Docker for local routing or configuring Valhalla for multi-modal analysis and now need to decide which one belongs at the center of your fleet stack, rather than which one is easiest to demo.
Evaluation Criteria and Test Setup
Before comparing feature lists, pin down the criteria that actually determine your production outcome. Most teams over-index on raw route latency and under-index on how often edge weights change and how much operational surface area a JVM-based engine adds compared to a memory-mapped C++ binary.
Criteria worth scoring explicitly:
| Criterion | Why it matters |
|---|---|
| P99 route latency | Determines whether the engine can sit in a synchronous request path or needs an async queue |
| Memory per graph extract | Drives instance sizing and horizontal scaling cost |
| Dynamic weight update path | Determines how fast traffic, closures, or LEZ penalties propagate to live queries |
| Multimodal / isochrone support | Needed if the product surfaces reachability, transit, or walk+transit trip planning |
| Freight and HGV constraint depth | Needed if any fraction of the fleet is a truck subject to weight, height, or hazmat rules |
| Operational maturity | Docker image quality, community size, upgrade cadence, and license terms |
Test environment (Python 3.9+):
# requires: requests, numpy (pip install requests numpy)
# Docker images used for the benchmark environment in this guide
docker pull osrm/osrm-backend:latest
docker pull valhalla/valhalla:latest
docker pull graphhopper/graphhopper:latest
Run all three engines against the same PBF extract with equivalent car profiles before drawing conclusions — comparing OSRM on a metro extract against GraphHopper on a full-country graph produces numbers that say nothing about the engines themselves. The benchmarking routing engine latency and accuracy page in this section builds a reusable harness for exactly this kind of apples-to-apples test.
Conceptual Architecture
The three engines solve the same shortest-path problem with structurally different preprocessing and query models, and that difference is what drives every trade-off below.
OSRM compiles a directed graph from OSM tags into either a Contraction Hierarchy (CH) or a Multi-Level Dijkstra (MLD) structure, then memory-maps the resulting binary files directly for the osrm-routed daemon. Routing logic lives entirely in a Lua profile evaluated at extract time — there is no server-side scripting hook at query time. This is the same preprocessing pipeline covered in deploying OSRM with Docker for local routing, and it is why OSRM is the fastest of the three for a single static cost function but the least flexible for per-request customization.
Valhalla partitions the world into a hierarchical tile set (local, arterial, highway levels) and evaluates a costing model — auto, truck, bicycle, pedestrian, multimodal — against edge attributes at query time. Costing parameters (use_hills, use_tolls, truck dimensions) are request-level JSON, so the same tile set serves many different vehicle profiles without rebuilding anything. This tile architecture is also what gives Valhalla native isochrone generation and GTFS-aware multimodal routing, as detailed in Valhalla configuration for multi-modal analysis.
GraphHopper builds a CH graph (optionally layered with ALT/landmarks for flexible queries) on the JVM, and exposes a custom_model JSON mechanism that lets a request adjust priority and speed factors against arbitrary encoded values — road class, surface, hgv access, and more — without a graph rebuild, provided the base profile was built in “flexible” mode. That query-time programmability is GraphHopper’s core differentiator, at the cost of JVM memory overhead and, for the deepest freight regulatory features, commercial licensing.
The practical consequence: OSRM wins when you need the lowest possible latency against a fixed cost function and can tolerate a rebuild cycle for weight changes; Valhalla wins when isochrones, elevation, or multimodal transit are core product requirements; GraphHopper wins when different requests need materially different weighting logic at the same instant, such as one truck avoiding a bridge weight limit while another avoids a low-emission zone.
Feature Coverage Comparison
| Capability | OSRM | Valhalla | GraphHopper |
|---|---|---|---|
| Turn restrictions and turn costs | Yes, from OSM relations | Yes, costed per transition | Yes, costed per transition |
| Time-dependent routing | Traffic overlay via re-customization only | Native, date_time input with predicted speeds |
Limited in core; deeper support in commercial tier |
| Alternative routes | Yes, alternatives=true, limited count |
Yes | Yes |
| Map matching | Yes, /match endpoint |
Yes, Meili matcher | Yes, dedicated map-matching module |
| Isochrones | No native endpoint | Yes, /isochrone |
Yes, matrix-approximated /isochrone |
| Elevation-aware costing | No | Yes, Skadi elevation service | Yes |
| GTFS / transit | No | Yes, embedded multimodal costing | Yes, separate PT routing mode |
| Query-time custom cost logic | No, requires re-extract | Partial, costing_options JSON |
Yes, full custom_model JSON |
| License | BSD 2-Clause | MIT | Apache 2.0 core, commercial add-ons |
The query-time custom cost logic row is the one teams underestimate. If your product needs one query to avoid tolls and the next to avoid unpaved surfaces, OSRM forces you to either run two separately-customized graphs or accept a shared compromise profile — neither Valhalla’s costing_options nor especially GraphHopper’s custom_model have that constraint.
Latency and Memory Footprint
Raw numbers below come from repeated single-route and 25x25 matrix queries against a metro-area extract (roughly 400k routable edges) on identical 8-core/32 GB hosts, MLD for OSRM and CH-based profiles for GraphHopper. Absolute numbers will shift with hardware and extract size — treat the relative ordering as the durable signal.
| Metric | OSRM (MLD) | Valhalla | GraphHopper |
|---|---|---|---|
| P50 single-route latency | 2-4 ms | 25-40 ms | 12-18 ms |
| P99 single-route latency | 8-15 ms | 60-90 ms | 30-50 ms |
| 25x25 matrix latency | 15-30 ms | 150-300 ms | 80-150 ms |
| Resident memory (metro extract) | 1.5-3 GB | 3-6 GB | 5-9 GB |
| Cold-start time | Seconds (mmap) | Tens of seconds (tile load) | 1-3 minutes (JVM warmup + graph load) |
OSRM’s latency advantage comes directly from its memory-mapped, precomputed hierarchy — there is effectively no per-request computation beyond hierarchy traversal. Valhalla pays a real cost for evaluating a full costing function against tile edges at query time, which is also what makes it flexible enough to support elevation and multimodal costing without a rebuild. GraphHopper sits between the two: CH gives it most of OSRM’s speed advantage for static profiles, but the JVM and the custom_model evaluation path add overhead versus OSRM’s raw hierarchy walk.
For fleets computing thousands of matrix cells per minute through async route matrix automation, this latency gap compounds quickly — a workload that saturates one OSRM instance may need three or four Valhalla instances behind a load balancer to hit the same throughput target.
Dynamic Weights and Live Traffic
How fast a cost change reaches production queries differs sharply across the three engines, and this is often the deciding factor for fleets running time-of-day pricing, incident-driven closures, or low-emission-zone penalties.
| Engine | Update mechanism | Typical propagation time |
|---|---|---|
| OSRM (MLD) | osrm-customize re-applies weights to an already-partitioned graph |
Minutes, no re-partition needed |
| OSRM (CH) | Full re-extract and re-contraction required | Tens of minutes to hours |
| Valhalla | Predicted speed buckets baked into tiles at build time; live traffic needs a real-time speed extension | Minutes (extension) to hours (rebuild) |
| GraphHopper | custom_model applied per request against live encoded values; base graph itself is static |
Immediate, no rebuild for weighting changes |
GraphHopper’s per-request model is the only one of the three that changes routing behavior without any server-side rebuild step at all — the trade-off is that the underlying graph topology and base speeds are still fixed until the next import, so it handles cost reweighting far better than it handles genuinely new road data. OSRM under MLD is the standard choice for teams applying a single shared traffic or penalty overlay to the whole fleet, a pattern covered in more depth in integrating custom traffic weights into OSRM. Valhalla is the weakest of the three here for genuinely live updates, but the best of the three for pre-computed time-of-day patterns baked into predicted speeds.
Multimodal and Isochrone Support
Valhalla was designed around multimodal trip planning from the start, and it shows: /isochrone and transit-aware multimodal costing are first-class endpoints, not bolted-on features. GraphHopper supports public transit routing through a distinct GTFS-aware mode and approximates isochrones by expanding a matrix and building a contour from reachable cells, which is workable but coarser and slower at scale than Valhalla’s native tile-based isochrone generation. OSRM has neither capability built in — teams that need isochrones on an OSRM-centric stack typically compute them externally with GeoPandas over /table responses, which is a materially different engineering investment than calling a dedicated endpoint.
If transit fusion is part of the roadmap, review GTFS multi-modal trip-planning automation before committing to an engine — the amount of custom time-expanded graph work you take on yourself depends heavily on whether the underlying engine already understands GTFS stop_times.
Freight and HGV Routing Capability
Freight is where the three engines diverge the most, because HGV routing needs more than a speed adjustment — it needs weight limits, height and width clearance, axle load, and hazmat restrictions evaluated per edge.
| Freight capability | OSRM | Valhalla | GraphHopper |
|---|---|---|---|
maxweight / maxheight enforcement |
Manual, via custom Lua profile | Native, truck costing parameters |
Native, custom_model + encoded values |
| Axle load and length | Not built in | Native | Native (commercial for deep coverage) |
| Hazmat restrictions | Not built in | Native, hazmat flag |
Commercial tier |
Conditional access (hgv:conditional=*) |
Manual parsing required | Partial | Commercial tier |
| Turn cost tuning for long vehicles | Manual | Native | Native |
OSRM requires you to encode every freight rule yourself in the Lua profile — a viable path if your fleet has one or two well-known vehicle classes, covered in configuring edge weights for freight logistics, but brittle if regulatory rules vary by jurisdiction. Valhalla’s truck costing model reads maxweight, maxheight, and hazmat tags directly and exposes them as request parameters, making it the strongest open-source default for mixed HGV fleets. GraphHopper covers the same ground in its open core for basic dimension constraints, and reserves conditional restrictions and deep regulatory datasets for its commercial offering. Turn restriction handling itself — a prerequisite for any credible HGV routing — is compared directly in setting turn restrictions in GraphHopper vs OSRM, and the full freight comparison with real HGV query examples lives in OSRM vs Valhalla vs GraphHopper for freight routing.
Decision Framework
Rather than picking a “winner,” score candidates against your own weighted criteria. This keeps the decision defensible when a stakeholder asks why you didn’t pick the fastest engine outright.
# requires: none (pure Python 3.9+)
from dataclasses import dataclass
@dataclass
class EngineScore:
name: str
latency: int # 1-5, 5 = fastest
memory: int # 1-5, 5 = lowest footprint
dynamic_weights: int # 1-5, 5 = fastest propagation
multimodal: int # 1-5, 5 = strongest native support
freight: int # 1-5, 5 = deepest HGV coverage
def weighted_score(engine: EngineScore, weights: dict[str, float]) -> float:
"""Combine per-criterion scores into a single weighted decision score."""
return (
engine.latency * weights["latency"]
+ engine.memory * weights["memory"]
+ engine.dynamic_weights * weights["dynamic_weights"]
+ engine.multimodal * weights["multimodal"]
+ engine.freight * weights["freight"]
)
engines = [
EngineScore("OSRM", latency=5, memory=5, dynamic_weights=3, multimodal=1, freight=2),
EngineScore("Valhalla", latency=2, memory=3, dynamic_weights=3, multimodal=5, freight=5),
EngineScore("GraphHopper", latency=4, memory=2, dynamic_weights=5, multimodal=3, freight=4),
]
# Example: an urban last-mile fleet with mixed van/HGV vehicles and no transit needs
last_mile_weights = {"latency": 0.30, "memory": 0.15, "dynamic_weights": 0.20, "multimodal": 0.05, "freight": 0.30}
ranked = sorted(engines, key=lambda e: weighted_score(e, last_mile_weights), reverse=True)
for e in ranked:
print(f"{e.name}: {weighted_score(e, last_mile_weights):.2f}")
How the three profiles typically shake out:
| Use case | Weight priorities | Common pick |
|---|---|---|
| High-QPS courier dispatch, car/van only | Latency and memory dominant | OSRM |
| Mixed HGV freight network with dimension rules | Freight coverage and dynamic weights | Valhalla or GraphHopper commercial |
| Multimodal accessibility or transit product | Multimodal and isochrone support dominant | Valhalla |
| Per-vehicle custom avoidance rules (tolls, LEZ, bridges) applied concurrently | Dynamic weights, query-time flexibility | GraphHopper |
Many production stacks do not pick one engine — they route each query type to whichever engine scores best for it, treating the routing layer itself as a small internal service rather than a single vendor choice.
Validation and Benchmarking
Before cutting any production traffic over to a new engine, run the same route set against the incumbent and the candidate and diff both distance and duration, not just HTTP status codes.
# requires: requests, numpy (pip install requests numpy)
import time
import requests
import numpy as np
def sample_latencies(url: str, n: int = 50) -> np.ndarray:
"""Fire n sequential requests and return latency in milliseconds."""
samples = []
for _ in range(n):
start = time.perf_counter()
resp = requests.get(url, timeout=5)
resp.raise_for_status()
samples.append((time.perf_counter() - start) * 1000)
return np.array(samples)
def compare_engines(osrm_url: str, valhalla_url: str, graphhopper_url: str) -> None:
"""Print P50/P95/P99 latency for identical routes across all three engines."""
for name, url in [("OSRM", osrm_url), ("Valhalla", valhalla_url), ("GraphHopper", graphhopper_url)]:
lat = sample_latencies(url)
p50, p95, p99 = np.percentile(lat, [50, 95, 99])
print(f"{name:12s} P50={p50:6.1f}ms P95={p95:6.1f}ms P99={p99:6.1f}ms")
compare_engines(
osrm_url="http://localhost:5000/route/v1/driving/-77.0365,38.8977;-77.0091,38.8895",
valhalla_url="http://localhost:8002/route",
graphhopper_url="http://localhost:8989/route?point=38.8977,-77.0365&point=38.8895,-77.0091",
)
Beyond raw latency, validate that route geometry and duration agree within an acceptable tolerance for a sample of real fleet trips, and check that all three engines return the same “no route” or “unreachable” verdict on known-disconnected coordinate pairs. A deeper, reusable harness with warmup handling and accuracy deltas against ground truth is built out in benchmarking routing engine latency and accuracy.
Troubleshooting
Can I run OSRM, Valhalla, and GraphHopper side by side for different vehicle profiles?
Yes, and it is a common production pattern. Route latency-sensitive car traffic through OSRM, send multimodal and isochrone queries to Valhalla, and use GraphHopper where per-request custom_model overrides are needed, all behind a single internal router service that picks the backend by request type.
Why does GraphHopper use more memory than OSRM for the same extract?
GraphHopper runs on the JVM and keeps its graph structures as in-memory Java objects with garbage-collection overhead, while OSRM memory-maps precomputed binary files directly from disk. The JVM heap plus the contraction hierarchy graph typically costs 1.5-3x the resident memory of an equivalent OSRM MLD deployment.
Does Valhalla support live traffic updates without rebuilding tiles?
Partially. Predicted and historical speed buckets are baked into tiles at build time and require a rebuild to change. Live traffic needs the real-time speed extension layered on top of existing tiles; it is less dynamic than OSRM’s osrm-customize step on an MLD graph, which reapplies new weights in minutes.
Is GraphHopper's truck routing free or Enterprise-only?
Basic weight, height, width, and length constraints work in the open-source core through custom_model and encoded values. Conditional restrictions, semantic road-attribute segmentation, and pre-built truck profiles with regulatory data are part of the commercial GraphHopper offering.
How do I decide between MLD-based dynamic weights and query-time custom models?
Use OSRM’s MLD re-customization when the same updated weights apply to every query, such as a nightly traffic overlay shared across all fleet vehicles. Use GraphHopper’s custom_model when different requests need different weighting logic simultaneously, such as one vehicle avoiding low bridges while another avoids toll roads, without any rebuild step.
Related
- OSRM vs Valhalla vs GraphHopper for freight routing — a deep, query-by-query comparison of HGV mass, dimension, and hazmat handling across all three engines
- Benchmarking routing engine latency and accuracy — a reproducible harness for percentile latency, warmup handling, and ground-truth accuracy deltas
- Deploying OSRM with Docker for local routing — the full preprocessing pipeline behind OSRM’s contraction hierarchy and MLD deployments
- Valhalla configuration for multi-modal analysis — tile-based costing models, isochrones, and multimodal transit setup
- Setting turn restrictions in GraphHopper vs OSRM — how each engine encodes and costs turn restrictions from OSM relations