Serving & Invalidating OSM Tiles Jump to heading
Generating a tile set is a batch problem with a clear end. Serving one is an operational problem with no end, and the decisions that make it cheap or expensive are made at packaging time, before a single request arrives.
The Problem This Topic Solves Jump to heading
You have an archive of tiles and readers who need to see them, on a map that must reflect the data without being regenerated from scratch every time an edit lands. The serving decision determines your ongoing cost and your operational surface; the invalidation decision determines whether the map is ever actually current.
The failure scenario is a map that is correct in the archive and wrong in the browser. A diff is applied, the affected tiles are regenerated, and readers continue to see the old ones — from a browser cache, from an edge location, or from a proxy nobody remembered was in the path. Nothing errors, the archive is demonstrably right, and the bug report says “the map is wrong” with a screenshot that cannot be reproduced.
Prerequisites Jump to heading
Understand the tile pyramid from OSM Vector Tiles & Rendering Pipelines, especially the ancestor relationship between zooms. Know how change files identify what moved, from Applying .osc Change Files with osmium. And have an archive to serve, from either Building OSM Tiles with Tippecanoe or Planetiler & Tilemaker Workflows.
Three Packaging Formats Jump to heading
MBTiles is a SQLite database holding tiles and metadata. It is the natural output of most generators, it is easy to inspect with ordinary tools, and it requires a process that can open the database to serve from it. Updating individual tiles is a straightforward row update, which makes it the friendliest format for incremental re-rendering.
PMTiles is a single file with a hierarchical index at its head, designed so a client can find and fetch one tile with a couple of HTTP range requests. That removes the server entirely: the archive sits in object storage behind a content delivery network and clients read it directly. The trade is that the file is immutable in practice — updating individual tiles means rewriting it — so it suits a tile set rebuilt as a unit.
Loose directories store one file per tile. Conceptually simple and trivially cacheable, but a continent at zoom 14 is hundreds of millions of objects, and most storage systems handle that number of small files badly. It remains reasonable for a small area or for a tile set with a shallow maximum zoom.
Invalidation: the Dirty Tile Computation Jump to heading
When a change file arrives, the great majority of tiles are unaffected. Working out which ones are not is the whole game, and it has three parts.
Locate the changes. Every created, modified or deleted element in the change file has a position — directly for a node, through its members for a way or relation. Those positions map to tile coordinates at the maximum zoom. The mechanics are in Computing a Dirty Tile List from an .osc File.
Expand to ancestors. A tile at zoom 14 has one parent at 13, one grandparent at 12, and so on to zoom 0. A change visible at 14 also changes the generalized representation at every lower zoom, so the dirty set must include the whole ancestor chain. Forgetting this produces the characteristic bug of a map that is right zoomed in and wrong zoomed out.
Expand for the buffer. A change near a tile boundary affects the neighbouring tile too, because that tile retains geometry past its edge. Including the eight neighbours at the deepest zoom is a cheap over-approximation that avoids a whole class of edge artefacts.
The result is a set of tile addresses to re-render and, separately, to purge from every cache layer.
Cache Layers and What Each Needs Jump to heading
Every layer between the archive and the reader needs its own invalidation, and they do not all support the same operations.
An edge network typically supports purging by URL or by a tag attached at response time. Tagging responses with the tile’s zoom and a coarse spatial key makes bulk purges possible without enumerating millions of URLs.
An origin cache — a reverse proxy in front of a tile server — usually supports purge by URL or a short time to live. For a tile set updated every few minutes, a time to live shorter than the update interval is simpler than wiring purges and costs little.
A browser cache cannot be purged at all. The only reliable control is the URL: including a version token in the tile path, incremented when the tile set is rebuilt, makes every tile a new URL and sidesteps the problem entirely. That is worth building in from the start, because retrofitting it means changing every style document.
Validation and Error Handling Jump to heading
| Condition | Root cause | Detection | Remediation |
|---|---|---|---|
| Map right zoomed in, wrong zoomed out | Ancestor tiles not invalidated | Low-zoom tiles predate the change | Expand the dirty set up the whole ancestor chain |
| Stale tiles after a purge | A cache layer nobody knew about | Response headers name an unexpected cache | Trace the full request path and purge each layer |
| Artefacts near changed features | Neighbouring tiles not invalidated | Seams appear only next to edits | Include neighbours at the deepest zoom |
| Readers see old tiles for days | Browser cache with a long time to live | Only a hard refresh fixes it | Put a version token in the tile URL |
| Rewrite takes longer than the update interval | Immutable format with frequent updates | Each rebuild overlaps the next | Use an updateable format, or update less often |
| Purge API rate limited | Enumerating millions of URLs | Purge requests throttled | Tag responses and purge by tag |
| Archive updated, server serves old data | Server holds the file open | Restart fixes it | Swap archives atomically and signal the server |
Performance and Scale Jump to heading
Tile serving is an unusually favourable workload: requests are read-only, responses are immutable for their lifetime, and the access distribution is extremely skewed — a small fraction of tiles serve the overwhelming majority of requests. That skew is what makes edge caching so effective and what makes origin capacity planning far less alarming than the tile count suggests.
Two consequences matter. Cache hit ratio is the metric, not origin throughput; a ratio in the high nineties is normal and achievable, and a drop in it is the earliest signal that something is wrong with cache keys or invalidation. And the long tail is cheap to serve slowly: tiles nobody looks at can be rendered on demand rather than pre-generated, which for deep zooms saves enormous amounts of generation time.
Compression matters too. Vector tiles compress well and clients accept gzipped payloads, so tiles should be stored compressed and served with the appropriate header rather than compressed per request.
Failure Modes and Gotchas Jump to heading
- Ancestors are not optional. A dirty set without the ancestor chain leaves low zooms permanently behind.
- Buffers make neighbours dirty. A change just inside one tile alters the geometry retained by the tile next door.
- Browser caches are unpurgeable. Only a URL change reaches them, which is why versioned paths are worth the effort.
- Immutable formats and frequent updates conflict. Pick one; a minutely-updated single-file archive is a rewrite treadmill.
- Open file handles hide updates. A server holding an archive open keeps serving the old contents after the file is replaced.
- Range requests need range support. Serverless delivery depends on the storage and every cache in front of it honouring range requests correctly.
- An empty tile is not a missing tile. Returning a not-found status where a legitimately empty tile exists makes clients retry endlessly.
Integration Points Jump to heading
Upstream, the dirty tile list comes from the replication stream — Incremental Updates for Derived Datasets covers the general pattern and tiles are one instance of it. Downstream, the style document consumes the archive’s metadata, so the layer names and zoom range recorded at build time are part of the serving contract.
The two guides develop each half: Serving PMTiles from Object Storage for delivery, and Invalidating Tile Caches After an OSM Diff for keeping it current.
Guides in This Topic Jump to heading
- Serving PMTiles from Object Storage — a tile set with no server at all, and what that requires of the storage layer.
- Invalidating Tile Caches After an OSM Diff — turning a change file into a purge set that reaches every cache layer.
Frequently Asked Questions Jump to heading
Why is my map correct when zoomed in and stale when zoomed out?
Because the invalidation computed the tiles a change touches at the deepest zoom and stopped there. A feature that changed at zoom 14 also appears, generalized, in the zoom 13 tile containing that one, and in its parent, and so on to zoom 0. The dirty set must include the whole ancestor chain of every touched tile — which is cheap, since each level contributes only a quarter as many tiles as the one below.
Do I need a tile server at all?
Not for a static base map. A single-file archive with an embedded index can be read directly by clients using HTTP range requests, so the whole serving stack becomes an object store and a content delivery network. A server earns its place when you need per-request behaviour — access control, filtering by user, dynamically composed layers — or when the tile set is updated incrementally and rewriting a large immutable file is impractical.
How do I invalidate a browser cache?
You cannot, which is why the answer has to be in the URL. Including a version token in the tile path — incremented whenever the tile set is rebuilt — makes every tile a new URL that no browser has cached, and lets you set a long cache lifetime on the old URLs without ever needing them to expire. Retrofitting this means updating every style document that references the tiles, so it is worth building in before the first reader arrives.
Should empty tiles be stored or omitted?
Omit them from storage and return a valid empty response for them, rather than a not-found status. Storing hundreds of millions of zero-length objects is a real cost in any storage system, and clients handle a missing tile as a transient failure to be retried rather than as an answer. An explicit empty tile — or a not-found the client is configured to treat as empty — avoids both problems.
What cache hit ratio should I expect?
High nineties for a map with ordinary usage, because tile requests are extremely skewed towards a small number of popular areas and zoom levels. That is also why the ratio is the metric to watch: a sudden drop usually means cache keys changed, a version token was introduced without the caches being warmed, or invalidation purged far more than it needed to.
Related Jump to heading
- OSM Vector Tiles & Rendering Pipelines — the parent section and the pyramid this serving layer delivers.
- Incremental Updates for Derived Datasets — the general pattern tiles are one instance of.
- Computing a Dirty Tile List from an .osc File — producing the input to every invalidation here.
- Generating MBTiles from OSM GeoJSON — the archive and metadata this layer serves.
- Building a Minutely Update Pipeline — the change stream that drives re-rendering.
- Measuring OSM Replication Lag in Seconds — how far behind the served map actually is.
Up one level: OSM Vector Tiles & Rendering Pipelines.