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.


Routing Engine Comparison Matrix Five-row comparison grid scoring OSRM, Valhalla, and GraphHopper on query latency, dynamic weight updates, multimodal and isochrone support, HGV freight capability, and memory footprint. OSRM vs Valhalla vs GraphHopper Five production-critical axes, scored from the same class of deployment Criteria OSRM Valhalla GraphHopper Query Latency Fastest 1-5 ms (CH) Moderate 20-80 ms tiled Fast 10-40 ms CH+ALT Dynamic Weights Rebuild-based osrm-customize (MLD) Build-time predicted speed buckets Query-time custom_model JSON Multimodal / Isochrones None car/bike/foot only Native isochrone + transit Partial PT plugin, matrix approx HGV / Freight Manual custom Lua profile Native weight/height/hazmat Native+ conditional restrictions Memory Footprint Lowest mmap CH/MLD files Moderate tiled hierarchy Highest JVM heap + graph

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 modelauto, 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.