Self-hosting
https://instantmap.org is a shared demo server. Run your own before launch:
- you control its uptime,
- your map traffic isn’t shared with anyone else’s,
- you can point it at your own tiles.
It is one Cloudflare Worker, and it takes about ten minutes to set up.
What you need
Section titled “What you need”| Cloudflare account | Workers Paid plan ($5/month). Rendering a tile takes 20–200 ms of CPU, more than the free plan allows per request. Cached images cost nothing to serve again. |
| Vector tiles | Any source in the Protomaps basemap schema, served as {z}/{x}/{y} MVT. See Tile sources. |
| Toolchain | Rust with the wasm32-unknown-unknown target, wasm-bindgen-cli 0.2.129, and Bun. |
Deploy
Section titled “Deploy”git clone https://github.com/williamrogersdev/instantmapcd instantmaprustup target add wasm32-unknown-unknowncargo install wasm-bindgen-cli --version 0.2.129bun installbun run buildIn worker/wrangler.jsonc, make these changes:
- Set
TILE_URLto your tile source. - Remove
account_idandroutes; they belong to instantmap.org. - Add your own domain, if you want one.
{ "name": "maps", "vars": { "TILE_URL": "https://tiles.example.com/{z}/{x}/{y}.mvt" }, "routes": [{ "pattern": "maps.example.com", "custom_domain": true }]}Then deploy:
cd worker && bunx wrangler deployPoint your maps at the new server:
<instant-map src="https://maps.example.com" …></instant-map>staticUrl({ src: "https://maps.example.com", … });Your server also serves /instant-map.js and the font, so you can load the client from it as well.
Tile sources
Section titled “Tile sources”| Source | TILE_URL |
Notes |
|---|---|---|
| Protomaps API | https://api.protomaps.com/tiles/v4/{z}/{x}/{y}.mvt?key=YOUR_KEY |
Fastest start. Needs a free key from protomaps.com. |
| Your own PMTiles on R2 | The URL of a PMTiles Worker in front of your bucket | Download a planet or regional .pmtiles build, upload it to R2, and deploy Protomaps’ serverless Worker. You own every part of the stack. |
| Any z/x/y server | Your URL with {z}, {x}, {y} |
Must use the Protomaps schema (layers earth, water, landuse, roads, boundaries, places, buildings). |
The Worker caches upstream tiles for 7 days, so your tile source sees roughly one request per tile per data centre per week (once per tile in total with the TILE_CACHE bucket below). A request to the tile source times out after 8 seconds and is retried once.
How pictures are cached
Section titled “How pictures are cached”The Worker is set up for Cloudflare’s own cache, with three layers. Workers Cache and the per-data-centre cache need no provisioning; create the R2 bucket as described below.
| Layer | What it does |
|---|---|
Workers Cache ("cache": { "enabled": true } in worker/wrangler.jsonc) |
Sits in front of the Worker. A repeated address is answered without running the Worker, so it costs no CPU time. It is tiered, so a picture drawn for one visitor is shared with other Cloudflare locations, and simultaneous requests for the same picture are drawn once. Each deployment starts a new cache. |
| The Worker’s own cache | Keyed by what the address means, not how it is written. The same picture asked for with its parameters in another order, or after a new deployment, is not drawn again in that data centre. |
| R2 (optional, below) | Shares rendered tiles across locations until the bucket lifecycle rule expires them. |
With Workers Cache on, Cloudflare bills requests for the site’s static files as Worker requests; without it they are free. If you serve only pictures from the Worker, this makes no difference. To turn it off, remove the cache line.
Optional: keep tiles in R2
Section titled “Optional: keep tiles in R2”The checked-in Worker configuration binds TILE_CACHE to an R2 bucket named instantmap-tiles. Create that bucket in your own account before deploying, or remove the r2_buckets block to use only the Cloudflare cache layers. With R2 enabled, an edge-cache miss checks the bucket before drawing a tile.
cd worker && bunx wrangler r2 bucket create instantmap-tilesThe binding in worker/wrangler.jsonc is:
"r2_buckets": [{ "binding": "TILE_CACHE", "bucket_name": "instantmap-tiles" }]Give the bucket a lifecycle rule, for example delete objects after 90 days, so it cannot grow without bound. Only tiles are stored. Static images are not.
Limit on uncached renders
Section titled “Limit on uncached renders”A request that has to be drawn costs CPU time. The ratelimits block in worker/wrangler.jsonc adds a rate limit binding named RENDER_LIMIT: one address may ask for 600 uncached pictures per 60 seconds at each Cloudflare location. Pictures that are already cached are never limited. A request over the limit gets 429 with Retry-After: 10. Change the numbers to suit your traffic, or remove the block to have no limit.
Each picture that is drawn writes one JSON line to Workers Logs: the tile or image size, the milliseconds spent fetching vector tiles (upstreamMs), the size in bytes and the Cloudflare location (colo). Cloudflare records the CPU time of each request alongside it. Cached pictures write nothing, because the Worker does not run for them.
Check it
Section titled “Check it”curl -sI "https://maps.example.com/static?center=0,51.5&zoom=10&size=400x300" | grep -iE "^(HTTP|content-type|cache-control)"You should see 200, image/jpeg and public, max-age=604800, immutable. Ask again and the response carries cf-cache-status: HIT.
The Worker also answers GET /health with { "ok": true, "renderer": "…" }, which suits an uptime check.