Cache invalidation and TTLs: strategies that work

TTLs, event-based deletes and versioned keys: choosing TTLs per data type, purging CDNs, avoiding stale reads after writes, and why eviction isn't invalidation.

9 min read
On this page 10 sections
  1. Why invalidation is hard
  2. TTL-based expiry
  3. Choosing TTLs
  4. Event-based invalidation
  5. Versioned keys
  6. Invalidation vs eviction
  7. Purging across layers
  8. Avoiding stale data right after writes
  9. Key takeaways
  10. Frequently asked questions

Cache invalidation means removing or replacing cached copies when the data behind them changes, so users stop seeing old answers. In practice you combine three tools: TTLs, so every entry expires on its own; event-based deletes, for when you know data has changed; and versioned keys or URLs, which make old copies unreachable instead of deleting them. The hard part isn't any one tool but knowing every place a copy lives.

Why invalidation is hard

Four things make it harder than "delete the key when the row changes":

  • Copies live in many layers: the browser, the CDN, a reverse proxy, Redis and the database's own materialized views.

  • Derived data fans out. One row feeds many cached values, and nothing records which ones.

  • Reads and writes race. A reader can put an old value back into the cache just after a writer deleted it.

  • Some caches can't be reached. You can purge a CDN, but not a student's phone.

Here is an illustrative case. After a mock test's results are out, a teacher corrects the answer key for question 17. Scores are recomputed by a background job, since they are data, not cache. But seven caches also hold something that just became wrong:

CacheWhat it holdsHow to clear it
Redis: question 17The question and its answerDelete the key after the correction commits
Redis: the test paperThe full paper with answersBump the test's version, so a new key is used
Redis: 20,000 result summariesEach student's score cardBump a version in their keys; don't delete 20,000 keys one by one
Materialized view: leaderboardRanks for every studentRefresh after re-scoring finishes
Redis: leaderboard JSONTop 100, cached for 30 secondsLet the TTL expire after the refresh
CDN and Nginx: public results pageThe toppers listPurge by tag or prefix at the CDN; Nginx's one-second microcache expires by itself
CDN and browsers: solutions PDFThe old answerPublish under a new file name; browsers can't be purged

No single mechanism covers all seven, which is why real systems mix TTLs, events and versions.

TTL-based expiry

A TTL makes every entry expire after a fixed time, whether or not anything changed. It needs no coordination, it caps staleness at the TTL, and it quietly repairs every invalidation you forgot or that failed. It is the one mechanism every cached entry should have.

The costs: data is stale for up to the full TTL, every expiry causes a miss even when nothing changed, and entries created together expire together, such as everything a warm-up job loaded just before a test. Add random jitter of about ±10% to spread those expiries out, and read about cache stampedes for hot keys that everyone reads at once.

Choosing TTLs

Choose a TTL by asking how stale this data can be before someone is harmed or confused, not how often it changes. Some starting points for a learning platform:

DataTTLReasoning
Fingerprinted static filesA yearA new version gets a new URL; nothing is ever invalidated
Course catalogue and syllabus10 to 60 minutes, plus delete on publishChanges are planned; brief staleness is harmless
Test paper during a live testThe whole test window, pre-warmedIt must not change mid-test; corrections get a new version
Leaderboard30 to 120 secondsStudents accept "as of a minute ago"
Student dashboard summary1 to 5 minutes, plus delete on the student's own actionsTheir own actions must show at once
Live-class viewer count5 to 10 secondsApproximate is fine
Feature flags and configuration30 to 60 secondsShort enough that switching something off works quickly
Enrolment and access rightsVery short or none, plus delete on changeA refund or a ban must take effect
Fees, payments and order statusNone, or a few secondsMoney needs the truth

Event-based invalidation

When your code knows data changed, it can remove the affected entries straight away instead of waiting for the TTL. Three rules make this reliable:

  1. Delete after commit, not before. Delete inside the transaction and another request can refill the cache from the old, still-committed row before your change lands. In Django, use transaction.on_commit; our guide to Django caching shows the code.

  2. Delete rather than update. Deletes are idempotent, so retries and reordering are harmless. Two updates that arrive out of order leave the older value in place.

  3. Keep the TTL anyway. The read-write race described in caching strategies can still leave a stale value behind, and the TTL is what eventually removes it.

Fan-out is the harder problem. Tags help: record which keys belong to a course, in a Redis set or through your CDN's cache tags, and invalidate the tag. At larger scale, invalidation can be driven from the database itself. Facebook's memcache paper describes daemons that read each database's committed statements and broadcast the resulting deletes to every cache cluster, so application code can't forget one. In PostgreSQL, logical decoding can feed a similar pipeline.

Versioned keys

Instead of deleting entries, change the name you look them up by. Store a version for each course, include it in every key (course:42:v1727591000:outline), and bump the version when the course changes. Every old entry becomes unreachable at once and ages out through its TTL or eviction.

The same idea covers static files: app.3f9c2a.css becomes app.8b1d04.css when its contents change, so browsers and CDNs fetch the new file without any purge. AWS recommends exactly this for CloudFront when files change often, noting that versioned file names work even for copies already held in users' browsers, and that they avoid invalidation charges.

  • Pros: one write invalidates any number of entries, there are no delete storms, and rolling back means pointing at the old version.

  • Cons: an extra read to fetch the version (cache it briefly in process), and orphaned entries that use memory until they expire or are evicted.

Versioned keys also save you from pattern deletes. Deleting "course:42:*" in Redis requires scanning the keyspace, and the KEYS command blocks Redis while it runs.

Invalidation vs eviction

InvalidationExpiryEviction
Why the entry goesThe data changedIts TTL ran outThe cache needed memory
Who decidesYour code or a purgeThe clockThe cache's eviction policy, such as LRU or LFU
What it protectsCorrectnessA limit on stalenessCapacity

Eviction is not a substitute for invalidation, because the cache evicts whatever its policy picks, not what changed. The policy also matters: in Redis, the volatile policies only evict keys that have a TTL, and the default, noeviction, makes writes fail when memory is full. Redis's eviction documentation describes each option, and our guide to Redis caching covers which one suits a cache.

Purging across layers

When data changes, clear caches from the inside out: the application cache first, then the reverse proxy, then the CDN. Clear the CDN first and its next miss may be refilled from a Redis entry you haven't deleted yet.

  • Application cache: delete or version the keys after the commit.

  • Reverse proxy: open-source Nginx has no selective purge (that's in the commercial NGINX Plus), so rely on short TTLs there, or change the URL.

  • CDN: purge narrowly by URL, tag or prefix. Cloudflare offers all of these on every plan, with rate limits; our guide to CDN caching covers purging without causing a stampede. On CloudFront, the first 1,000 invalidation paths each month are free and each path after that is charged, a wildcard path counts as one, and tag-based invalidation counts against the same allowance, according to AWS's pricing notes.

  • Browsers: can't be purged. Keep HTML on no-cache or short lifetimes, and give everything long-lived a versioned URL.

Avoiding stale data right after writes

The most visible staleness is a student's own change failing to appear: they update their profile or finish a lesson, reload, and see the old state. Users forgive a leaderboard a minute behind; they don't forgive their own action vanishing. Four fixes, from simplest to strongest:

  1. Render the response from the write. After saving, show the page using the data you just saved, not a fresh cached read.

  2. Update the cache as part of the write for the entity the user just edited, which is write-through for that one key.

  3. Bypass the cache briefly for the writer. Store a "fresh until" time in the session, a few seconds ahead, and skip caches for that user until it passes.

  4. Read from the primary database after a write. A read replica can lag behind for a moment, so a cache refilled from a lagging replica stores old data under a fresh TTL. See read replicas for routing reads after writes.

Key takeaways

  • Give every cached entry a TTL; it bounds staleness and repairs the invalidations you miss.

  • Choose TTLs by how much staleness would harm or confuse someone, and add jitter so entries don't expire together.

  • For known changes, delete after the commit, and keep the TTL as a safety net against races.

  • Use versioned keys and fingerprinted URLs to invalidate many entries, or unreachable browser copies, at once.

  • Clear layers from the inside out, and make a user's own writes visible immediately.

Frequently asked questions

What is cache invalidation and why is it hard?

Cache invalidation is removing or replacing cached data once the original has changed. It is hard because copies live in many places at once (browsers, CDNs, proxies and application caches), one change can affect many cached values that nothing tracks, concurrent reads can put stale data back just after a delete, and some caches, such as a user's browser, can't be reached at all. That's why TTLs remain a necessary safety net.

What is cache invalidation strategy?

A cache invalidation strategy is the set of rules that decides when cached data stops being used. The common ones are TTL-based expiry, event-based deletion when data changes, versioned keys or URLs that make old copies unreachable, and write-through updates that refresh the cache as data is written. Most systems combine them: a TTL on everything, events for known changes, and versions for static files and grouped data.

What is cache invalidation in CloudFront?

In Amazon CloudFront, an invalidation removes files from edge caches before they expire, so the next request fetches the current version from your origin. You submit paths such as /images/logo.jpg or /courses/*, or cache tags. The first 1,000 paths each month are free and later paths are charged, with a wildcard counting as one path. AWS recommends versioned file names instead for files you update often.

Share this article

Looking for something else?

Talk to Us