Build1 distinct publisher3 min readUpdated
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
Compiled by The EngineerSomething wrong?How this is made
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.
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].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The author argues this is a substantial amount of machinery for a server whose main job is to be a fast, boring translator between PostGIS and MVT, and that most deployments do not need it because most tile data does not change under the reader.
With cache_size_mb: 0, Martin builds each tile with ST_AsMVT on demand on every request, through pgbouncer, against tables holding millions of rows.
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.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
First-hand reproduction, single publisher, no external corroboration
The core mechanism is evidenced concretely and reproducibly for one deployment: byte-identical 242 B tiles before an edit, after it and thirty seconds later, with the correct tile appearing immediately after a container restart, while database, layer view and API returned the new geometry throughout. That is strong falsification of the alternative explanations the author checked. Evidence stops short of high confidence because there is exactly one source and one publisher, no upstream documentation, issue tracker or changelog reference for the cache's invalidation behaviour, no version pinning for Martin or PostGIS, and no quantitative measurement of the uncached path's cost.
One disclosed production deployment
Adoption evidence is limited to a single production system: one Martin instance serving roughly 2.67 million features with the in-memory cache disabled and an nginx five-second micro-cache in front. That is a real, months-old production disclosure rather than a demo, which keeps the score above floor, but there is no second deployment, no user or download figure, no upstream acknowledgement and no indication that the zero-cache pattern is used elsewhere.
Claims slightly conservative relative to the evidence shown
The framing is unusually well bounded for its evidence. The author refuses the tidy conclusion, states explicitly that disabling the cache is not general advice, discloses the ST_AsMVT-per-request cost through pgbouncer, and then reveals the nginx five-second cache that complicates the headline. The only mild overreach is generalising from one deployment to what 'most deployments' need. Net position is essentially aligned, tipping marginally to understated because the demonstrated finding is presented as a workload-specific choice rather than a defect claim about the project.
Practitioner write-up, no disclosed commercial stake
Incentive distortion appears low. The piece is a first-person engineering postmortem on a developer blogging platform, praises the project it discusses ('genuinely excellent', explicitly not a complaint'), sells nothing, names no employer, product or vendor, and volunteers facts that weaken its own headline. Residual incentive is the ordinary reputational reward for a clean debugging story, which favours a memorable framing such as setting a cache to zero.
Mechanism credible, generality unverified
Confidence is moderate. Within the described deployment the causal chain is well supported by reproducible observation and a decisive restart test, and the author's self-limiting framing reduces the risk of overstatement. But the cluster has a single source and publisher, no version information, no upstream confirmation that the behaviour persists in current releases, and no performance measurement for the uncached configuration, so claims about applicability beyond this one internal GIS remain unverified.
build
The only way to prove a contract test can fail is to ship a server that lies1 distinct publisher
build
The failure modes of AI agents in low-level code have names, and names can be gated in CI1 distinct publisher
build
A Rust veteran's first Zig project: the friction was tooling and layout, not safety1 distinct publisher
build
Waku 0.1.0 bets the product is the control plane, not another coding agent1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 20, 2026