Valhalla routing in WebAssembly for browsers, Node.js and experimental Cloudflare Workers. This repository provides two ESM packages with TypeScript declarations and a shared compiled WASM runtime:
| npm package | Runtime | Execution |
|---|---|---|
| valhalla-browser | Browser | Dedicated Web Worker; supports bundlers and direct CDN imports |
| valhalla-server | Node.js and experimental Cloudflare Workers | Dedicated Node worker thread, or a Cloudflare Worker isolate |
Both packages calculate routes with Valhalla and load standard graph tiles on
demand through HTTP ranges from an indexed TAR, or as individual .gph objects.
The Cloudflare adapter also supports private R2 bindings. Graph datasets are
hosted separately from the routing engine.
Demo · API documentation · Source · Release setup
The examples use version 0.2.1. Both SDKs are published on npm; the local tarball instructions below are available for testing workspace changes.
pnpm add valhalla-browser
# npm install valhalla-browser also works for consumers.
No asset-copy plugin, Docker or native compiler is needed in your application.
The worker and WASM are located automatically. All
https://routing.example.com/datasets/your-release-id/... URLs below are
placeholders, not a hosted dataset service. Replace the complete manifest URL
with your deployment's actual URL, including its immutable release directory.
That directory must match the manifest's release value. Choose route coordinates
inside your dataset's coverage. See the
data preparation guide
to build compatible graph data.
Use this in a browser module or Vite application:
import { createRouter } from 'valhalla-browser';
const router = await createRouter({
// Replace with your deployment's versioned dataset manifest URL.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
});
try {
const result = await router.route({
origin: { lat: 47.0666667, lon: 9.5 },
destination: { lat: 47.2397558, lon: 9.5262874 },
});
console.log(result.native);
} finally {
await router.dispose();
}
These coordinates are covered by the historical July 2015 Liechtenstein benchmark, not current navigation data. © OpenStreetMap contributors, ODbL 1.0. Graphs are supplied separately and are not included in the package.
Results preserve Valhalla's full native JSON: distances in kilometers, times
in seconds, and leg shapes encoded as polyline6. Driving (auto), two
locations, English directions, a 30 m correlation radius and minimum
reachability 0 are the validated defaults.
Use the separate valhalla-server package for server execution. It includes
compiled WASM and self-contained TypeScript declarations. Import
valhalla-server/node for a dedicated Node worker thread, or
valhalla-server/cloudflare for the experimental HTTP/private-R2 adapter.
All adapters use the same WASM binary, graph format and four travel profiles.
pnpm add valhalla-server
import { createRouter } from 'valhalla-server/node';
const router = await createRouter({
// Replace with your deployment's versioned manifest URL.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
});
try {
const result = await router.route({
origin: { lat: 47.0666667, lon: 9.5 },
destination: { lat: 47.2397558, lon: 9.5262874 },
});
console.log(result.native.trip.summary);
} finally {
await router.dispose();
}
See Node.js and Cloudflare Workers for R2 bindings, per-request contexts, queue limits, cancellation and resource constraints. Cloudflare support has a local workerd proof; it remains experimental and is not a production capacity guarantee. The demo and docs stay on Pages.
The SDK supports driving (auto, the default), cycling (bicycle),
walking (pedestrian), and truck (truck). Each dataset must advertise
the requested profile in its manifest. await router.initialize() returns
supportedCostings; older auto-only datasets continue to support driving only.
Transit and combined bike-and-train journeys are not supported.
Use costing_options for the selected profile. Omit settings to use Valhalla's
defaults. This example uses the same historical Liechtenstein graph:
import { Router, type RouteRequest } from 'valhalla-browser';
const router = new Router({
// Replace with your deployment's multi-profile dataset manifest.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
});
const endpoints = {
origin: { lat: 47.1392862, lon: 9.5227962 },
destination: { lat: 47.1450, lon: 9.5168 },
};
const requests: RouteRequest[] = [
{ ...endpoints, costing: 'bicycle',
costing_options: { bicycle: { bicycle_type: 'road', cycling_speed: 24, use_roads: 0.2 } } },
{ ...endpoints, costing: 'pedestrian',
costing_options: { pedestrian: { walking_speed: 4 } } },
{ ...endpoints, costing: 'truck',
costing_options: { truck: { height: 2.5, width: 2, length: 6, weight: 5, axle_load: 2, hazmat: false } } },
];
try {
for (const request of requests) {
const result = await router.route(request);
console.log(result.native);
}
} finally {
await router.dispose();
}
Speeds are km/h, vehicle dimensions are meters, and weights are metric tonnes.
Profile changes reuse the same worker and tile cache. Unknown profiles or profiles
absent from the dataset reject with UNSUPPORTED_COSTING; invalid settings reject
with INVALID_REQUEST.
See Travel profiles and options for supported settings, defaults, dataset upgrades, and current limits. This guide is also included in the generated API documentation.
The package exports request, response, option, error and diagnostics types. Create one router per session and dispose it when the application no longer needs it. Only one native operation runs at a time; concurrent submissions queue.
import { Router } from 'valhalla-browser';
import type { RouteRequest, RouteResult, RouterOptions } from 'valhalla-browser';
const options: RouterOptions = {
// Replace with your deployment's versioned dataset manifest URL.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
memoryBudgetBytes: 32 * 1024 * 1024,
// Optional: these are the defaults, in labels per search vector, not bytes.
searchMemory: { astar: 16_384, bidirectionalAstar: 16_384, clearReservedMemory: false },
};
const request: RouteRequest = {
locations: [{ lat: 47.0666667, lon: 9.5 }, { lat: 47.2397558, lon: 9.5262874 }],
costing: 'auto',
};
const router = new Router(options); // Lazy: the engine loads on initialize/route.
try {
const result: RouteResult = await router.route(request);
console.log(result.native.trip.summary);
} finally {
await router.dispose();
}
Importing valhalla-browser is safe without browser globals, but creating its
Router requires a browser. For server execution, import valhalla-server/node
or the experimental valhalla-server/cloudflare adapter instead.
Search reservations grow when a route needs more labels; they are not search or
memory limits. The defaults override the dataset's large A* reservations without
changing its tiles, costing, or configuration file. Existing hosted datasets need
no changes. initialize() reports the effective searchMemory settings and an
effectiveConfigSha256, alongside the original dataset configuration hash.
See Diagnostics and memory
for tuning and measured reservation results
for the native-equivalence checks and memory baseline.
Save this as an HTML file and serve it over HTTP. No bundler is required. Keep the package version pinned in the import URL.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Browser-local route</title>
<button id="route">Calculate route</button>
<pre id="result"></pre>
<script type="module">
import { createRouter } from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/index.js';
const output = document.querySelector('#result');
const button = document.querySelector('#route');
button.onclick = async () => {
button.disabled = true;
let router;
try {
router = await createRouter({
// Replace with your deployment's versioned dataset manifest URL.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
});
const result = await router.route({
origin: { lat: 47.0666667, lon: 9.5 },
destination: { lat: 47.2397558, lon: 9.5262874 },
});
output.textContent = JSON.stringify(result.native, null, 2);
} catch (error) {
output.textContent = `${error.code ?? 'ERROR'}: ${error.message}`;
} finally {
await router?.dispose();
button.disabled = false;
}
};
</script>
</html>
UNPKG and ordinary static hosting can serve the same distribution. A cross-origin
import uses a small blob module-worker bootstrap; all routing still runs inside
the worker. Serve JavaScript as text/javascript and WASM as application/wasm,
with CORS permitting your application origin.
For CSP, allow the SDK origin in script-src and connect-src, the graph origin
in connect-src, blob: in worker-src, and 'wasm-unsafe-eval' in script-src.
If blob workers are disallowed, self-host matching assets and use the overrides
shown below. See the API site's CDN and CSP guide for a complete policy.
An AbortSignal or router.cancel() terminates the worker, rejects all its
outstanding operations and discards its decoded cache. The next route creates
a new worker. Termination also stops uninterrupted native CPU work.
import { createRouter, RoutingError } from 'valhalla-browser';
const router = await createRouter({
// Replace with your deployment's versioned dataset manifest URL.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
});
const request = {
origin: { lat: 47.0666667, lon: 9.5 },
destination: { lat: 47.2397558, lon: 9.5262874 },
};
try {
const controller = new AbortController();
const pending = router.route(request, { signal: controller.signal });
controller.abort(); // In a UI, call this from the Cancel button handler.
try {
await pending;
} catch (error) {
if (!(error instanceof RoutingError) || error.code !== 'CANCELLED') throw error;
}
const recovered = await router.route(request);
console.log(recovered.native.trip.summary);
} finally {
await router.dispose();
}
Keep the router alive between requests to reuse decoded graph tiles. Browser HTTP cache and CDN cache are separate; repeated range reuse is browser-dependent.
import { createRouter } from 'valhalla-browser';
const router = await createRouter({
// Replace with your deployment's versioned dataset manifest URL.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
});
const request = {
origin: { lat: 47.0666667, lon: 9.5 },
destination: { lat: 47.2397558, lon: 9.5262874 },
};
try {
await router.route(request);
const repeated = await router.route(request);
console.log(repeated.diagnostics.loader.requests); // Zero for this warm route.
console.log(repeated.diagnostics.decodedCacheHits);
console.log(await router.diagnostics());
} finally {
await router.dispose();
}
The cache budget bounds retained decoded tiles, not total worker memory. Heap high-water diagnostics measure allocated WASM memory capacity, not live objects or browser RSS. Diagnostics expose tile URLs and IDs; the SDK sends no telemetry.
Browser, Node and Cloudflare share one WASM binary with a 1,024 MiB upper ceiling.
wasmMemory configures each instance's initial allocation and hard growth cap:
128 / 512 MiB by default in browsers, 256 / 512 MiB in Node, or 64 / 96 MiB in Cloudflare.
Use whole MiB with 64 <= initialMiB <= maximumMiB <= 1024; resolved settings
are available as router.startup.wasmMemory. This limits native heap, stack and
static data, while JavaScript and other host overhead consume additional memory.
memoryBudgetBytes separately bounds decoded tiles. A larger maximum does not
allocate that amount at startup or increase a platform's own memory allowance.
indexed-tar is the default. individual-tiles uses full GETs for the same
standard tiles. A session pins a compatible, immutable manifest release.
import { Router, RoutingError } from 'valhalla-browser';
const router = new Router({
// Replace with your deployment's versioned dataset manifest URL.
manifestUrl: 'https://routing.example.com/datasets/your-release-id/manifest.json',
transport: 'individual-tiles',
timeoutMs: 10_000,
retries: 2,
// Hard WASM linear-memory cap; separate from the decoded-tile cache budget.
wasmMemory: { initialMiB: 64, maximumMiB: 256 },
onProgress: event => console.log(event.phase),
// Optional matching assets hosted on your application's origin:
// workerUrl: '/sdk/0.2.1/worker.js',
// wasmUrl: '/sdk/0.2.1/valhalla-browser.wasm',
});
try {
await router.route({
origin: { lat: 47.0666667, lon: 9.5 },
destination: { lat: 47.2397558, lon: 9.5262874 },
});
} catch (error) {
if (error instanceof RoutingError) {
console.error(error.code, error.message, { retryable: error.retryable });
} else {
throw error;
}
} finally {
await router.dispose();
}
Errors distinguish invalid input, coverage, no-route, transport, integrity,
compatibility and cancellation failures. Transient download errors are never
converted into missing tiles or false no-route results. Initialization can be
retried after failure. timeoutMs bounds fetches and worker loading; WASM startup
receives at least ten seconds per attempt. An opaque worker-load failure or WASM
initialization timeout gets at most one startup retry in a fresh worker;
retries: 0 or a custom workerFactory disables this recovery. Native routes are
never replayed. Relative asset overrides resolve against the page.
Provide your own compatible OSM-derived graph dataset. The repository includes
pnpm run data:osm to build standard tiles, Valhalla's native indexed graph.tar,
and the SDK manifest/configuration from an .osm.pbf extract. It uses the pinned
native Valhalla tools and preserves access for driving, cycling, walking, and truck routing.
See Build graph data from OpenStreetMap for a reproducible small example, custom extracts, native/container invocation, coverage bounds, timezone/admin inputs, static hosting and deployment validators. The same guide is included in the API documentation site's navigation.
Use the object-storage hosting guide to deploy datasets on S3, R2, MinIO or another compatible provider. It covers dashboard setup without AWS CLI, bucket CORS, cache rules for entire prefixes, HTTP metadata, byte ranges, deployment ETags and browser verification. It also explains stale CORS responses and CDN cache statuses. The guide is included in the generated API documentation.
Use Node 22.22.2 and the pinned pnpm 12.4.2. The five packages are the
browser SDK (valhalla-browser), server SDK (valhalla-server), private shared
core (@tobilg/valhalla-core), demo (@tobilg/valhalla-browser-demo) and
documentation (@tobilg/valhalla-browser-documentation). SDKs/demo use Vite 8.3.0.
git clone https://github.com/tobilg/valhalla-wasm.git
cd valhalla-wasm
pnpm install --frozen-lockfile
pnpm build:docs # No WASM, native toolchain or graph needed.
pnpm preview:docs # http://localhost:8081
pnpm build # Requires existing verified native artifacts.
pnpm dev # http://localhost:8080
pnpm pack:sdk # build/package/valhalla-browser-0.2.1.tgz
pnpm pack:server # build/package/valhalla-server-0.2.1.tgz
# Test local changes in another application:
pnpm add /absolute/path/to/valhalla-browser-0.2.1.tgz
A clean checkout needs the explicit native/data/WASM build steps in the
development guide.
The native pins remain Valhalla 3.8.3 at
a60c7cbfc83e073f50887cd27e0109d02e6b64e5, Emscripten 6.0.0,
Protobuf 21.12 and zlib 1.3.1. Package consumers do not need this toolchain.
pnpm test
pnpm test:package
pnpm test:cdn-import
pnpm test:docs
pnpm test:examples
pnpm test:browser
pnpm test:server:node
pnpm test:server:cloudflare
pnpm test:server:package
pnpm test:demo
The root README is also the TypeDoc homepage and is copied into the
valhalla-browser npm package. valhalla-server has its own package README.
Set all five package versions with pnpm run version:set 0.1.0 (or
npm run version:set -- 0.1.0), substituting your next stable version. The command
also keeps the SDK version references in this README and the development guide
in sync. See the
release guide
for the remaining release steps.
Guides and API comments are maintained with the source. Stable version tags
publish both verified SDKs through npm trusted publishing, then deploy documentation
to valhalla-browser-api and the demo to valhalla-browser on Cloudflare Pages. See
release setup
for the initial npm publication, trust configuration, Pages setup and dry runs.
For the hosted demo, set the GitHub repository variable VITE_DEMO_MANIFEST_URL
to your public R2 manifest URL for the Liechtenstein 2015 graph. The build bundles
the same journey presets and downloads graph tiles from that URL. Locally, the
same environment variable can be passed to pnpm run dev or pnpm run build:demo.
The demo displays routes over an interactive OpenStreetMap basemap with pan/zoom, endpoint markers, a basemap toggle and a Fit route button. Background map images come from OSM; routing still runs locally in the WASM worker. The current basemap may differ from the historical 2015 graph, and synthetic fixture roads are fictional. See the development guide for basemap configuration and hosting requirements.
Desktop Chromium, Firefox and WebKit are the browser verification targets. Physical mobile devices, other bundlers and worldwide coverage are not established. There is no OPFS, IndexedDB, service-worker cache, offline guarantee or external routing fallback. Bike-and-train routing remains separate discovery work.
See verification, MinIO testing, and R2 CORS policy.
Both SDKs are MIT licensed. Compiled dependencies' license texts are included under
dist/licenses/; preserve these notices when redistributing the runtime.