Inventory Your Caches to Stop Stale Data
"I saved it, but the screen still shows the old value." Reports like this are hard to reproduce and frustrating to triage. In most cases the cause is not a coding slip but a design gap: nobody decided where data is cached and for how long. This article, aimed at the design phase, shows how to inventory your cache layers and fix the invalidation triggers before you implement.
Why stale data shows up
Caches are usually added later for performance. "It's slow, let's cache it here too" repeated a few times leaves a system where no one knows every layer. After an update, one layer is fresh and another is still old.
- The list page is up to date, but the detail page is not
- It looks fixed on your machine, but a teammate still sees the old data
- A reload fixes it, then it goes stale again a few minutes later
All of these are symptoms of cache layers and lifetimes that live only in someone's head.
The cache inventory table
Before implementing, list every cache on the path the data travels. At minimum, check these four layers.
Layer | Examples | How lifetime is set | Invalidation trigger |
|---|---|---|---|
Browser | HTTP cache, Service Worker | Cache-Control max-age | Versioned URLs, revalidation |
CDN | Firebase Hosting, CloudFront | s-maxage, CDN settings | Purge, hashed file names |
Application (server) | Rails.cache, memoization | expires_in | Delete the key on update |
Client state | SWR, React Query, Redux | staleTime, etc. | Refetch after mutation (invalidate) |
Any row with an empty "Invalidation trigger" cell is a future bug. A simple rule helps: do not start implementing until every cell is filled.
What to decide per layer
1. What may be cached
Static parts of public pages are good candidates. Data visible only to the signed-in user, or data that must look fresh right after a change (such as the result screen after saving), should be kept off the CDN. Mark per-user responses explicitly with Cache-Control: private, no-store.
2. How long it may live
Decide, per feature, how stale the data is allowed to be. A ranking can be five minutes old; a value the user just saved cannot. Writing the tolerated staleness into the spec avoids later arguments about whether something is a bug.
3. Who invalidates it
Put the invalidation next to the update, in one place. A Rails example:
def update
@article.update!(article_params)
Rails.cache.delete(["article", @article.id])
Rails.cache.delete(["article_list", @article.project_id])
endWhen updates are scattered, one path always forgets to invalidate. Keep the update and its invalidation in the same method.
Triage order for "it didn't update" reports
Work from the layer closest to the user outward.
- Browser: Reproduce in a private window, or with "Disable cache" on in DevTools Network. If that fixes it, it is the browser layer
- Browser (response): Open the request in the Network tab. Does the Size column say
(disk cache)? Check Cache-Control and Age in the response headers - CDN: Look at the
AgeorX-Cacheheaders to see whether the CDN served the response - Application: Check server logs to see whether the request reached the database or was served from cache
- Client state: Confirm that a refetch runs after the update and review the library's cache settings
Record which layer fixed it. The gaps in your inventory table will show up on their own.
A failure example: each layer is right, the whole is wrong
The server cleared its cache on update, the CDN held responses for 60 seconds, and the browser for 5 minutes. Each setting was reasonable alone, yet together updates stayed invisible for up to about six minutes. Lifetimes add up across layers; keep that in mind when estimating at design time.
Putting it into practice with Bugoon
A "not updated" report cannot be triaged without details about the reporter's environment. With the Bugoon widget embedded, each report arrives with a screenshot plus the browser, OS, screen size, recorded operation steps, and console and network errors. Developers can start the first steps of the triage order above without asking follow-up questions. Linking to GitHub Issues lets you file the inventory update as a task, and the MCP server lets Claude Code or Cursor pick up the report for investigation.
Streamline bug reporting for your team.
Bugoon is free to get started. Add one line of code to your site and transform how your team handles bugs.
Get Started