Rebuilding a routing graph is routine; promoting it without dropping traffic is not, and the default approach — restart the container — costs both in-flight requests and a cold-start window. This page covers the shared-memory swap that avoids both, as part of deploying OSRM with Docker for local routing within Python Routing Engines & Isochrone Mapping.

The mechanism is OSRM’s named shared-memory datasets. Two names alternate; one serves while the other is loaded; a pointer moves. The care is in probing before the pointer moves and in not freeing the old dataset too eagerly.

When to use this approach

Use it whenever the routing service has an availability expectation and the graph is rebuilt on a schedule — so, most production deployments. It matters more as extract size grows, because cold-start time scales with the graph and a restart window that is tolerable on a city becomes unacceptable on a country.

It is also what makes frequent traffic refreshes viable. The customise-and-swap cycle described in integrating custom traffic weights into OSRM depends on the swap being cheap enough to run every few minutes.

What each promotion strategy costs in served traffic A restart drops in-flight requests, refuses connections for several seconds, and then serves elevated latency while the graph faults in. A shared-memory swap serves continuously, with latency unchanged either side of a cutover lasting under a second. Serving behaviour across a graph promotion restart serving normally refused cold — p99 ×40 warm again 18 s of refused connections, then 40 s of degraded latency blue-green serving from dataset A serving from dataset B B loaded and probed in the background cutover under a second, no refused connections, no cold window The cost is memory: both datasets are resident during the overlap, so provision for twice the prepared graph.

Implementation

The daemon has to be started against a named dataset rather than against files on disk.

# Serve from shared memory under a name, not from the .osrm files directly
docker run -d --name osrm --network host \
  -v /srv/osrm:/data \
  --memory 24g --memory-swap -1 \
  osrm/osrm-backend \
  osrm-routed --algorithm mld --shared-memory --dataset-name metro-a \
              --threads 8 --max-table-size 2000

--shared-memory with --dataset-name is what decouples the process from the artefact. Without it, osrm-routed maps the files directly and the only way to change graphs is to restart.

Staging the new build into the idle name is a single command that does not touch the serving one:

# Load the freshly customised artefacts into the currently idle slot
docker run --rm -v /srv/osrm:/data osrm/osrm-backend \
  osrm-datastore --dataset-name metro-b /data/metro-latest.osrm

Probing before cutover is the step that turns a swap into a safe promotion:

# requires: httpx (pip install httpx)
import httpx


PROBES = [
    # (name, coordinate pair, expected distance metres, tolerance)
    ("cross-city", "4.895,52.370;4.845,52.355", 6_400, 0.15),
    ("ring-road", "4.830,52.400;4.960,52.330", 18_900, 0.15),
    ("known-oneway", "4.889,52.373;4.891,52.373", 420, 0.30),
]


def probe(base_url: str) -> list[str]:
    """Return a list of failures; empty means the dataset is safe to promote."""
    failures = []
    for name, coords, expected_m, tol in PROBES:
        try:
            r = httpx.get(f"{base_url}/route/v1/driving/{coords}",
                          params={"overview": "false"}, timeout=10.0)
            r.raise_for_status()
            body = r.json()
        except Exception as exc:
            failures.append(f"{name}: request failed — {exc}")
            continue
        if body.get("code") != "Ok" or not body.get("routes"):
            failures.append(f"{name}: {body.get('code')}")
            continue
        got = body["routes"][0]["distance"]
        if abs(got - expected_m) / expected_m > tol:
            failures.append(f"{name}: {got:.0f} m vs expected {expected_m} m")
    return failures

The probe set is doing two jobs. Reachability catches a graph that loaded but is broken; distance tolerance catches a graph that is subtly different from the one that was validated, which is usually a sign the wrong artefact was staged.

Running the probes against the staged dataset requires a second daemon bound to a different port and the idle name — cheap, because it shares the already-loaded memory rather than duplicating it.

# Temporary daemon over the staged dataset, for probing only
docker run -d --name osrm-probe --network host \
  osrm/osrm-backend \
  osrm-routed --algorithm mld --shared-memory --dataset-name metro-b \
              --port 5001 --threads 2

Cutover then restarts only the serving daemon, pointed at the validated name. Because the data is already resident in shared memory, the new process is warm immediately — the restart costs milliseconds rather than the tens of seconds a cold load would.

Multi-instance deployments add one wrinkle worth planning for. Shared memory is per host, so a fleet of four routing nodes behind a load balancer has four independent staging and cutover operations rather than one. Running them simultaneously means every node is mid-swap at the same moment, which is safe but leaves no instance on the previous graph if the new one turns out to be bad in a way the probes missed.

The better pattern is a rolling promotion: drain one node from the balancer, swap it, probe it against real traffic patterns for a few minutes, and only then proceed to the next. It takes longer, and in exchange a defect that survives the synthetic probes is caught with three quarters of the fleet still serving the known-good graph. For a fleet small enough that draining one node materially reduces capacity, do the swap during the trough rather than skipping the rolling discipline — the capacity headroom is easier to arrange than a fleet-wide rollback under load.

Key parameters and tuning

Parameter Recommended value Notes
Dataset names two fixed values Alternating names, tracked in a state key
Memory provision 2× prepared graph Both datasets resident during the overlap
Probe count 3–6 routes Enough to cover reachability and distance sanity
Distance tolerance 10–20 % Wide enough to survive legitimate graph change
Probe port separate Never expose the staging daemon to production traffic
Old dataset retention until the next swap Free it eagerly and rollback stops being cheap
Swap state Redis or a state file Builder and loader must agree which name is live

Integration points

With the traffic refresh loop. Frequent customise cycles depend on this swap being cheap, and the alternating-name discipline is the same one described in traffic-incident-triggered re-routing. Sharing one state key between the incident path and the scheduled rebuild avoids the two racing each other.

With the build gates. A dataset should only reach staging if it has cleared the connectivity audit in auditing connectivity after every graph rebuild. The probe is a last line of defence, not the primary one — three routes cannot cover a national network.

With the matrix cache. A swap changes the graph version, which invalidates cached travel times by design. Publishing the new version to the cache key at the same moment as the cutover keeps the two consistent — see caching and invalidating travel-time matrices.

Promotion sequence, with rollback A completed build is staged into the idle dataset name, probed on a separate port, and promoted only if the probes pass. The previous dataset stays resident, so a rollback is the same cutover operation pointed the other way. Nothing reaches production traffic until the probes pass build + gates stage into idle name osrm-datastore probe on port 5001 no production traffic cut over probe fails — discard, keep serving A rollback: point back at A, which is still resident Freeing A immediately after cutover saves memory and removes the only cheap rollback you have — keep it until the next swap. The memory profile of a blue-green swap A 12 gigabyte graph holds 12.1 gigabytes in steady state and 24.3 during the overlap while both datasets are resident, returning to 12.1 only when the previous dataset is freed at the following swap. Memory resident through a swap on a 12 GB graph steady state, A only 12.1 GB staging B 24.3 GB — the peak after cutover, both held 24.3 GB — rollback available after next swap frees A 12.1 GB Provision for the peak. A host sized for 12 GB evicts pages on every swap and produces the cold latency this technique avoids. Holding both after cutover is deliberate — freeing A immediately saves nothing useful and removes the only fast rollback available.

Validation checklist

  • No requests are refused during a swap. Run a steady load through the cutover and assert zero connection errors and no 5xx responses.
  • Latency is unchanged either side. Compare p99 in the minute before and after. A spike means the new dataset was not resident when the pointer moved.
  • A failing probe blocks promotion. Stage a deliberately broken artefact and confirm the cutover does not happen.
  • Rollback completes in under a second. Time it. If it takes tens of seconds, the previous dataset was freed and is being reloaded.
  • Memory headroom survives the overlap. Confirm resident size during a swap stays below the container limit; exceeding it triggers eviction and the cold-start penalty you were avoiding.