Skip to content
Go back

OpenRouteService: One Stack, Many APIs

By SumGuy 11 min read
OpenRouteService: One Stack, Many APIs

The Collection Problem

If you’ve been following the self-hosted maps series, your docker-compose.yml is starting to look like a conference schedule. Nominatim for geocoding. OSRM for routing. A tile server for basemaps. PostGIS underneath all of it. Photon if you want fast autocomplete. Maybe Pelias if you need multi-source geocoding. Every piece is excellent at its job. Every piece is also its own container to manage, its own data pipeline to feed, its own port to remember.

Here’s the thing: most workloads don’t need the sharpest tool at every layer. They need good enough across all layers, deployed by one person, running on one server, answering before their next coffee break.

That’s the problem OpenRouteService solves. One stack. Directions, isochrones, matrix, geocoding, elevation. All of it behind one API, served out of one container, fed by one OSM extract. The Swiss Army knife approach to self-hosted mapping — not the sharpest blade in every category, but you only have to carry one knife.

Full example: Compose + extract pipeline at github.com/KingPin/sumguy-examples/tree/main/self-hosting/openrouteservice-all-in-one

What ORS Actually Is

OpenRouteService comes out of HeiGIT — the Heidelberg Institute for Geoinformation Technology, affiliated with Heidelberg University. These are people who study geographic information systems for a living, and they’ve been running a public instance of ORS at openrouteservice.org for years. The self-hosted version is the same software. You’re not running a stripped-down community fork.

The routing core is built on GraphHopper, a solid Java-based routing engine. ORS wraps it with an opinionated configuration system and layers on the additional APIs — isochrones, matrix, geocoding via Pelias, and elevation data from SRTM. The whole thing ships as a single Java application, which is refreshing compared to the usual microservices-all-the-way-down philosophy.

The public API also has a free tier with rate limits, so you can test against it before committing to the self-hosted setup. Just swap the base URL and your API key for your local instance URL. That transition is genuinely that clean.

What you get in one deployment:

That last one — Vroom-based route optimization — is the kind of feature that usually requires a separate paid service or a lot of bespoke code. ORS includes it.

Hardware Reality Check

Let’s be honest about what “one stack” costs. ORS is a Java application built around GraphHopper graphs. Those graphs are stored in memory during operation, and they are not small.

For a single US state extract — say, California — expect:

For a full US extract or a European country, double those numbers. For full-planet, you’re looking at 32 GB+ RAM just for runtime. The planet is not a home lab use case.

The practical sweet spot for home use is a regional PBF covering exactly what you need. If your application serves a metro area, use a state or sub-region extract. A Ryzen 5 mini PC with 16 GB RAM handles a single-state US extract comfortably at modest QPS.

How does this compare to running the pieces separately? Running Nominatim + OSRM + Pelias individually is actually heavier in aggregate — each has its own data import, its own RAM footprint, its own disk overhead. ORS consolidates the graph and the Pelias geocoding into a system that’s leaner than the sum of its parts. You trade per-service optimization for operational simplicity, and for most use cases, that’s the right trade.

The Compose Deployment

The official image is openrouteservice/openrouteservice. You’ll need your OSM extract as a .pbf file on the host — ORS reads it on first start and builds the routing graphs from it. Graph building happens inside the container, not during image pull. Plan for that time.

docker-compose.yml
services:
ors:
image: openrouteservice/openrouteservice:latest
container_name: ors
ports:
- "8080:8082"
volumes:
- ./ors-data:/home/ors
environment:
ORS_URL: http://localhost:8080/ors
JAVA_OPTS: "-Xms2g -Xmx6g"
ors.engine.source_file: /home/ors/files/region.osm.pbf
ors.engine.profiles.car.enabled: true
ors.engine.profiles.bike-regular.enabled: true
ors.engine.profiles.foot-walking.enabled: true
restart: unless-stopped

Before starting, drop your PBF into ./ors-data/files/:

Terminal window
mkdir -p ors-data/files
# Download a regional extract from Geofabrik
curl -L https://download.geofabrik.de/north-america/us/california-latest.osm.pbf \
-o ors-data/files/region.osm.pbf
docker compose up -d
docker compose logs -f ors

Graph building takes 5–30 minutes depending on extract size and your hardware. Watch for a log line like Graphs were built successfully. Once that appears, ORS starts accepting requests.

The JAVA_OPTS env var controls heap. -Xmx6g caps the JVM at 6 GB. Set this based on your actual RAM — if you have 16 GB and you’re running a medium extract, -Xmx8g gives the JVM room to work without starving the rest of the system. Don’t set it higher than ~70% of available RAM.

Profile selection matters for both build time and runtime footprint. Only enable the profiles you actually use. Building car + bike + foot is the common case. Adding HGV (heavy goods vehicles), wheelchair, or e-bike adds significant graph weight.

Isochrones: The Killer Feature

Routing is useful. Isochrones are the thing that makes people’s eyes go wide in demos.

An isochrone answers: “Given this point, what area is reachable within N minutes by this mode of transport?” The result is a polygon — or a set of concentric polygons for multiple time thresholds — you can render on a map, use for geospatial joins, or feed into spatial queries.

Use cases:

The API call is simple:

Terminal window
curl -X POST http://localhost:8080/ors/v2/isochrones/driving-car \
-H "Content-Type: application/json" \
-d '{
"locations": [[-122.4194, 37.7749]],
"range": [900, 1800],
"range_type": "time"
}'

That asks for 15-minute and 30-minute drive-time isochrones around San Francisco’s city center. range values are in seconds. Swap driving-car for foot-walking or cycling-regular to get the same polygon for pedestrian or bike travel.

The response is GeoJSON — polygons ready to drop into any mapping library, PostGIS table, or QGIS layer:

{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[...]]
},
"properties": {
"value": 900,
"center": [-122.4194, 37.7749],
"area": 47.3
}
}
]
}

The area property gives you the reachable area in square kilometers. Handy for quick sanity checks — if a 15-minute walk comes back as 200 km², something is wrong.

Matrix API: Fleet Logistics Without a SaaS Contract

The directions API gives you A→B routing. The matrix API gives you all the A→B distances at once, for many origins and many destinations simultaneously.

Concrete example: you have 5 delivery vehicles and 50 drop points. You need to know the travel time from each vehicle’s current location to each drop point to build an optimal assignment. That’s a 5×50 matrix — 250 route calculations. You could loop and call /directions 250 times. Or you call /matrix once and get all 250 values in a single response.

Terminal window
curl -X POST http://localhost:8080/ors/v2/matrix/driving-car \
-H "Content-Type: application/json" \
-d '{
"locations": [
[-122.4194, 37.7749],
[-122.4089, 37.7853],
[-122.3988, 37.7651]
],
"metrics": ["duration", "distance"],
"resolve_locations": false
}'

Response gives you durations and distances as 2D arrays — row = source, column = destination. Everything in seconds and meters respectively. Feed this directly into your scheduling logic, your VRP solver, or the built-in Vroom optimization endpoint.

For logistics use cases, this is where ORS earns its keep. A standalone OSRM install can do matrix calculations too, but you’d be running OSRM separately, without the isochrones, without Pelias geocoding, and without Vroom optimization. ORS gives you the full logistics toolkit in one place.

API Ergonomics

ORS has a proper OpenAPI spec at http://localhost:8080/ors/openapi.json once your instance is running. Swagger UI is available at http://localhost:8080/ors/swagger-ui — useful for exploration and testing without writing curl commands.

Official client libraries exist for Python (openrouteservice) and JavaScript (openrouteservice-js). Both support the self-hosted use case by letting you configure a custom base URL. The swap from public API to local instance is literally one parameter:

ors_client.py
import openrouteservice
# Public API
client = openrouteservice.Client(key="your-api-key")
# Self-hosted
client = openrouteservice.Client(
base_url="http://localhost:8080/ors"
)
# Everything else works the same
route = client.directions(
coordinates=[[-122.4194, 37.7749], [-118.2437, 34.0522]],
profile="driving-car"
)

The directions response includes turn-by-turn instructions, elevation profile if enabled, distance, duration, and the route geometry as encoded polyline or GeoJSON. Useful query parameters for the /directions endpoint:

The documentation is genuinely good. Every endpoint has examples, parameter descriptions, and response schemas. It’s a refreshing contrast to “here’s a README with three lines and a broken example.”

When ORS Is the Right Call

ORS makes sense when you want to answer yes to most of these:

If you’re building a consumer app that needs to handle thousands of geocoding requests per minute, run Nominatim separately with proper hardware and tuning. If you need hyper-accurate bicycle routing with elevation-aware profiles and custom surface weighting, BRouter or Valhalla go deeper on that. If you’re running a global service and raw routing throughput is the only metric that matters, OSRM is faster at pure point-to-point routing — it does less, which means it does that one thing with less overhead.

But if you’re a developer, a data analyst, or a small shop who needs the full toolbox — who doesn’t want to spend a week standing up and integrating five separate services — ORS is the answer. One docker compose up, one data pipeline, one API to learn.

The Verdict

The self-hosted maps ecosystem is genuinely excellent and also genuinely fragmented. Every specialized tool is a separate project, a separate data import, a separate operational concern. ORS takes a clear position: trade per-component optimization for operational simplicity, and do it without compromising on features.

Isochrones alone are worth the deployment for a lot of use cases. The matrix API is table stakes for any logistics workload. The Pelias geocoding removes the need for a separate Nominatim install for most cases. The Vroom optimization is the kind of bonus feature that usually costs money.

If you’re looking at a pile of individual OSM service containers and thinking “there has to be a better way” — there is. It’s one container, one PBF, one API. Your 2 AM self will appreciate not having to remember which port Nominatim is on versus which port OSRM is on.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Boundary vs Teleport

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts