A Map Cache Was Fast Until the Page Cache Lied

One of my recent performance fixes was supposed to be straightforward: calculate a map viewport from saved ZIP codes, store the bounds, and stop making geocoding requests whenever a profile page loads. The data layer worked. Repeated actions correctly reported that the cached bounds were still valid, and fresh browser traces showed no calls to the geocoding service. Yet the public page kept serving the old map configuration. The bug was not in the algorithm. It was a second cache, outside the code path I had just fixed, confidently returning yesterday’s HTML.
The expensive work was easy to identify
The page displayed a geographic service area from many postal codes. Resolving and fitting those values on every request wasted time and external API capacity. I moved that work to a controlled update action, saved the north, south, east, and west bounds, and let the frontend consume four numbers.
This changed the request from “rebuild the geography” to “render known state.” It also made the result auditable. I could inspect the stored bounds, compare them with the intended region, and repeat the action without additional API calls when nothing had changed.
The first successful test was misleading. Inside WordPress, the new metadata was correct. The action returned the expected message. Theme assets had the new version. But the normal public URL still included a null viewport because a hosting-level page cache had a ten-minute copy of the old HTML.
This is the dangerous kind of cache failure: every layer tells a locally true story. The database says the save worked. The application cache has been cleared. The browser receives a fast 200 response. Only the combined system is wrong.
I started naming every cache owner
I wrote down the layers rather than using “the cache” as one vague noun: saved WordPress metadata, object cache, plugin cache, hosting page cache, browser cache, and the external geocoding provider. For each layer I asked what key it used, what event invalidated it, and what evidence proved a miss.
The missing step was a guarded per-post purge for the host’s page cache. I added it only when the hosting API was available, preserving a no-op path for installations on other infrastructure. The metadata format did not need to change, so rollback stayed source-only.
A fresh request is part of the test
After the fix, I stopped treating a correct admin response as completion. I loaded the ordinary public URL, recorded the cache response header, confirmed the first request missed, and checked that the next request returned a hit with the same viewport. Then I opened a fresh browser trace and verified zero geocoding traffic.
That sequence tests both correctness and the reason for the optimization. A page that shows the right map while still calling the provider is incomplete; a page that avoids the provider but shows stale geography is also incomplete.
The invalidation checklist I now use.
- Identify the authoritative saved value.
- List every cache that can contain a rendered copy.
- Invalidate the narrowest affected key after a successful save.
- Confirm the immediate public request is a miss.
- Confirm the following request is a hit with identical output.
- Trace external calls to prove the expensive work stayed removed.
I avoid global purges when a per-page purge exists. Broad invalidation can hide dependency mistakes and create unnecessary load across unrelated pages.
Performance work is state-management work
Caching is often sold as a speed technique, but the engineering problem is state coordination. A cache is correct only when its lifetime matches the data it represents. Adding more layers without clear ownership can make pages faster at being wrong.
The business version is just as direct: if staff update a service area, the public page should change promptly, and the site should not buy the same calculation on every visit. Both promises belong in the acceptance criteria.
The rollback stayed deliberately boring. I did not introduce a new cache schema or migrate the stored viewport. The release added a guarded invalidation call after the existing save and left installations without that host integration unchanged. That meant the previous theme version could be restored without reversing database state.
Before rollout, I recorded a known profile’s exact bounds, current asset version, and cache headers. After installation I checked all public profiles for successful responses and verified that every profile with usable location data had bounds. A second representative profile with hundreds of postal values proved the approach was not tuned to one small dataset. The release evidence tied source commit, artifact checksum, automated checks, public HTTP results, and browser network behavior together. That chain made rollback a decision backed by measurements rather than a nervous guess.
Now I draw the invalidation path first
For any cache-related change, I would now draw the read and invalidation path before touching code. Then I would verify it from outside the application, using public response headers and a network trace. The most important cache in the system is often the one the feature code cannot see.
Photo by Vladimir Srajber on Pexels.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


