Caching Strategy for Geospatial APIs

Caching Strategy for Geospatial APIs

E
By Etzal Earth
13 min read

A geospatial API has a peculiar traffic shape. Most of the surface of the planet is never requested at all. A small set of places, the centre of a capital city, the ports, the airports, the neighbourhood where somebody is running a site study this week, absorb an enormous share of the reads. That skew is the strongest argument for putting a cache in front of a spatial service, and it is the reason such a cache can look extraordinarily effective on the day it is switched on.

The trouble shows up when the key space is examined. Latitude and longitude are continuous. Two requests about the same street corner, issued by two different callers, will almost never carry the same coordinate pair. One rounds to six decimals, one passes a raw GPS fix straight through, one has been reprojected and back a couple of times and now differs in the eighth decimal place. The traffic concentrates on the same places while the cache keys scatter across the reals, and a naive cache can post a near zero hit rate on traffic that is, semantically, almost all repeats.

Everything interesting about caching a geospatial service lives in the gap between those two facts. The work is not picking a cache. It is deciding what counts as the same question.

Why the same places keep being asked about

Spatial demand is not uniform, and it is not close. Population is clustered, economic activity is more clustered than population, and analytical interest is more clustered still. A hazard API gets asked about the places where a hazard is unfolding. A places API gets asked about commercial districts. A building API gets asked about whatever a customer is underwriting this quarter.

Agent traffic sharpens the concentration further. A model working through a site selection task will query the same candidate area from several angles: buildings, then roads, then flood extent, then nearby amenities. If the runtime retries or the agent reformulates, the same area gets hit again within seconds.

This is the good news, and it should be quantified before any design decisions get made rather than assumed. Log the requested geometries, quantise them coarsely, and count. If demand is genuinely spread thin, the effort is better spent making the origin fast.

Why the key space fights back

Cache keys demand exact equality. Coordinates do not offer it.

Three separate mechanisms break naive keying. First, precision. A caller who sends 13 decimal places produces a key nothing else will ever match, and the extra digits describe a distance smaller than an atom. Second, representation. The same number arrives as a string, a float, in scientific notation, with a trailing zero, or with a minus sign on a zero. Third, semantics. A bounding box that is one metre wider than a cached one is a different key, but the answer is almost certainly identical.

Radius queries add their own problem. A caller asking for everything within 500 metres of a point and a caller asking for 520 metres around a point 15 metres away want almost the same answer, and no key normalisation makes them equal. Continuous parameters do not quantise into a lookup table without changing what is being answered.

The consequence is that a spatial cache cannot be bolted on at the transport layer and left alone. Key construction has to be a deliberate part of the API design.

Quantising coordinates, and what the precision costs

The standard move is to snap the request to a grid before it becomes a key. Round the coordinates, or encode them into a geohash or a similar hierarchical cell identifier, and use that as the lookup.

The arithmetic is worth holding in your head. A degree of latitude is roughly 111 kilometres everywhere. Three decimal places is therefore on the order of a hundred metres, four is on the order of ten metres, five is about a metre. Longitude shrinks with the cosine of latitude, so the same decimal place buys a tighter distance near the poles and the full distance at the equator. Any grid built on rounded degrees is anisotropic, and that is usually acceptable as long as it is understood rather than discovered later.

The precision choice is a direct trade. Coarse cells give high hit rates and wrong answers near boundaries. Fine cells give exact answers and a cache that never hits. The right resolution depends entirely on the gradient of the data being served:

  • Elevation over flat terrain tolerates coarse quantisation. Elevation on a cliff edge does not.
  • Administrative lookups break badly at coarse resolution, because the whole point of the answer is which side of a line the point falls on.
  • Weather fields are already gridded upstream, so quantising below the source grid resolution buys nothing and costs hit rate.
  • Building and address lookups need metre level precision, because the answer changes between neighbouring parcels.

The discipline that settles most of these arguments is to set the quantisation from the resolution of the underlying data. Keying below the source resolution invents distinctions the data cannot support, and keying above it throws away distinctions it can.

Tile alignment beats ad hoc rounding

For anything served as a layer rather than a point answer, snapping to the tile scheme is better than rounding coordinates. Web map tiling divides the world into a quadtree of tiles at each zoom level, so every point at a given zoom belongs to exactly one tile, tiles nest cleanly across zooms, and the identifier is three small integers.

Tile keys give several things that ad hoc rounding does not. They are stable, so the same tile identifier means the same extent forever. They compose, so a client requesting a viewport requests a handful of known tiles rather than one unique bounding box. They deduplicate across clients, because two users looking at roughly the same area request literally the same tiles. And they make prefetching sane, since the neighbours of a tile are trivially computable.

The cost is that the caller has to accept tile boundaries. A request for a custom polygon has to be answered either by fetching covering tiles and clipping, which returns more data than asked for, or by falling through to the origin. Offering both is reasonable: a tile endpoint that is fast and cacheable, and a polygon endpoint that is slower and honest about it.

Requests that are inherently unique should be identified as such rather than pretended into the cache. An arbitrary polygon drawn by a user, or an analysis with twelve tunable parameters, is a cache miss by nature.

The layers, from edge to computed artefact

A real spatial service ends up with at least three caches, and they should not be confused with one another.

The edge caches complete HTTP responses close to the caller. It works on immutable or near immutable content, which in practice means tiles, static extracts, and versioned artefacts. It is the only layer that reduces both origin load and network latency, and it is by far the cheapest hit.

The origin caches the results of expensive reads: a database query, a fetch to an upstream provider, a rendered vector tile. This is where most keys are constructed and where the quantisation rules live. It absorbs the traffic that the edge cannot, either because the response varies by caller or because the content is too dynamic for long edge lifetimes.

Underneath sits the artefact layer, and this is where the largest wins usually are. Computed artefacts are the things you build once and read many times: a spatial index, a pre-joined table of places to administrative regions, a rasterised hazard extent, a precomputed set of aggregates per cell. They are not caches in the eviction sense, because they should be built deliberately and kept until superseded. Treating them as caches leads to an expensive artefact being quietly evicted under memory pressure and rebuilt on the critical path of a user request.

Lifetime belongs to the data, not the endpoint

A single global TTL is the most common mistake in this design, and it is wrong in both directions at once. It makes hazard data stale and building data needlessly volatile.

The rates genuinely differ by orders of magnitude. Administrative boundaries change on the timescale of legislation. Building footprints change with construction and with the release cadence of whatever dataset supplies them, which is typically monthly or slower. Road geometry in a crowd sourced dataset changes continuously but matters at the scale of days. Flood extents and active disaster alerts change within hours and are worthless once stale. Aircraft and vessel positions are stale in seconds.

The lifetime should be derived from the update cadence of the source, not chosen for operational convenience. Where a source publishes on a schedule, the natural expiry is the next scheduled publication, not a fixed duration. Where a source publishes irregularly, the honest approach is a short lifetime plus revalidation.

Two refinements make the difference between a cache that helps and one that causes incidents. The first is stale while revalidate: serve the cached copy immediately, refresh in the background, so a slow upstream degrades freshness rather than availability. The second is a hard staleness ceiling beyond which a stale answer is refused rather than served, so that a permanently broken upstream produces an honest error instead of a confidently wrong reading from last week.

Whatever the policy, the response should carry the age of the data it contains. A caller, particularly an automated one, cannot reason about whether a figure is usable without knowing when it was true.

Never cache a partial result

This is the rule that costs the most when it is broken, and it is broken constantly, because the code path that breaks it looks like resilience.

The pattern goes: a request needs five upstream sources, four respond, one times out. The service degrades gracefully, assembles a response from what it has, returns it with a two hundred status, and the caching layer, which sees only a successful response, stores it. For the next several hours every caller receives the degraded answer. Nobody sees an error. The missing layer simply does not appear, and downstream consumers treat its absence as an absence in the world rather than an absence in the pipeline.

The fix is structural, not a matter of care. The object that gets written to the cache has to know whether it is complete. Every assembly step records which sources contributed and which failed, and the write is gated on that record. Concretely:

  • A response assembled from a full set of sources is cached at its normal lifetime.
  • A response missing any contributing source is either not cached at all, or cached with a lifetime measured in seconds purely to absorb a retry storm.
  • The completeness record travels in the response body, so a caller can distinguish an empty layer from an unavailable one.
  • Cache writes happen at one chokepoint, so the gate cannot be bypassed by a new code path added six months later.

This deserves a rule rather than a code review comment because the failure is silent. A partial result that is never cached causes a visible error and gets fixed. One that is cached for six hours becomes a data quality problem discovered weeks later in somebody else's analysis.

Invalidation, which is where this actually gets hard

Populating a cache is easy. Emptying the right part of it is the problem, and in a spatial system the right part is defined geographically, which nothing in a standard cache understands.

An upstream release updates one region. A correction lands on one building. A hazard extent grows and now covers cells that previously had none. In every case the set of keys to invalidate is spatial, and it corresponds to no prefix or tag recorded at write time.

Three approaches work, in increasing order of effort:

  • Version the namespace. Include a dataset version in every key. A new release changes the version, the old keys become unreachable and age out on their own. This is the simplest correct answer and it should be the default. Its cost is that a small correction invalidates everything.
  • Tag by cell. Record, at write time, which coarse cells a cached entry depends on, and invalidate by cell when a region changes. This gives targeted invalidation at the price of a dependency index that has to be maintained.
  • Invalidate by dependency graph. For derived artefacts that combine several sources, track what each artefact was built from and rebuild what is affected. This is the correct answer for the artefact layer and it is real engineering work.

Two rules of thumb survive contact with production. Short lifetimes are a legitimate substitute for invalidation on anything cheap to recompute. And a change that alters the meaning of a field, rather than its value, should never be handled by invalidation at all: version the response format instead, because a cache holding two incompatible shapes under interchangeable keys is worse than a cold cache.

What the edge does to your accounting

The consequence that surprises people has nothing to do with correctness. A read served from an edge cache never reaches the origin, and therefore never reaches whatever counts requests for metering, quotas, rate limits, or analytics.

This matters in several directions. Usage reporting undercounts, and the undercount is biased towards exactly the popular, cacheable endpoints. Rate limits applied at the origin do not restrain a caller whose traffic is absorbed at the edge. Per caller attribution disappears, because a shared edge object is by definition not caller specific. And any signal built on request logs, such as which places are in demand, is measuring cache misses rather than demand.

There is no free fix, but there are honest choices. Meter at the edge if the platform supports it, and accept that the counts and the origin logs will disagree. Meter at the origin and state plainly that cached reads are not billed, which is a defensible and even attractive policy. Or keep any response that must be metered per caller out of the shared cache entirely, which is the correct answer for anything carrying caller specific content anyway.

What should not happen is for the discrepancy to be discovered during a billing dispute. Decide which number is the number, and document it.

The decision that matters

If only one thing from this survives into your implementation, make it the completeness gate. Precision tuning is reversible: pick a resolution, measure, adjust. Lifetimes are reversible. Invalidation strategy can be upgraded when the pain justifies it.

A degraded response written into a cache is not reversible in the same way, because by the time anyone notices, it has been read, stored, joined and reported on by people who had no way to know it was incomplete. The cache did exactly what it was told. It was told to remember an answer that should never have been given.