Skip to content

Build1 publisher3 min readPublished

A Tile Cache With No Invalidation Hook Belongs at Zero, With a Comment Saying Why

A vector tile server has run in production for months with its cache deliberately set to 0 MB. The stale-tile bug that got it there is a lesson in reading what a config option actually refreshes.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • A vector tile server config in production for months, serving 2.7 million features to a web GIS, contains the line cache_size_mb: 0, a deliberately disabled cache.
  • The config comment under that line reads: MUST stay 0. Martin's in-memory tile cache has no invalidation hook for underlying data changes.
  • Martin is a vector tile server written in Rust; pointed at PostGIS it discovers spatial tables and views and serves each one as an MVT endpoint.
  • The deployment sits in front of a road-inventory database of 1.8 million point features, 697,000 lines and 172,000 polygons, spread across roughly a hundred layers.
  • Users do not only view the data; they edit it (move a sign, redraw a kerb line, correct an attribute) and expect the map to show the change.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A vector tile server that has been in production for months, serving 2.7 million features to a web GIS, runs with `cache_size_mb: 0` [1]. Directly above that line sits a comment the operator wrote for the next person to touch the file: MUST stay 0, because Martin's in-memory tile cache has no invalidation hook for underlying data changes [2].

Martin is a vector tile server written in Rust that points at PostGIS, discovers spatial tables and views, and serves each one as an MVT endpoint [3]. In this deployment, writing on dev.to, the author has it in front of a road-inventory database of 1.8 million point features, 697,000 lines and 172,000 polygons across roughly a hundred layers [4]. Those three counts sum to 2,669,000, which is where the 2.7 million figure comes from [18]. The distinguishing detail is not the size. It is that users edit the data, moving a sign or redrawing a kerb line, and expect the map to show it [5].

The cache defaults to 512 MB and defaults to on, which is the right default for nightly static extracts [6]. The author left it on for that reason, and then a user reported that an edited geometry still looked wrong on the map [7]. The four obvious suspects, browser cache, nginx cache, an uncommitted edit and an edit in the wrong layer, all cleared: the database, the layer view and the API each returned the new geometry, and the map did not [8].

The diagnosis is the part worth stealing. A tile is a file, so fetch it and count bytes. Before the edit, 242 B. After the edit, 242 B. Thirty seconds later, 242 B, byte-identical rather than merely similar [9]. It was still 242 B when the container was restarted, at which point it immediately became the correct tile [10]. That is the signature of a cache with no expiry path at all: a cached tile is served unchanged for the lifetime of the process, with no TTL to wait out and no hook that notices the rows moved [11].

The trap next to it is `reload_interval`, which re-discovers the catalog, meaning which tables and views exist, their geometry columns and their SRID, and makes a newly created layer appear as a tile source without a restart [12]. It does not touch cached tiles. The two settings sit near each other in the config, and the author reports assuming for a while that one covered the other [13].

Correct invalidation is not a small omission. The server would need to know a row changed, work out which tiles at which zoom levels held the old geometry and which hold the new one, and evict all of them, which means a change feed from Postgres plus geometry-to-tile-index math for both states at every zoom level [14]. The author's judgement is that this is a lot of machinery for a fast translator between PostGIS and MVT, and that most deployments do not need it because most tile data does not change under the reader [15].

The cost of zero is real and stated: every request now builds its tile with `ST_AsMVT` on demand, through pgbouncer, against tables holding millions of rows [16]. That is survivable here only because the data is not public, and the tool is internal with a bounded number of authenticated users [17].

What to watch is whether that boundedness holds. The moment this map is exposed to unauthenticated readers, or the layer count grows, the arithmetic behind `cache_size_mb: 0` changes and the answer becomes a cache with an eviction path in front of it, not the process cache that has none [11][16][17].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories