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.
On this page 10 sections
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:
| Cache | What it holds | How to clear it |
|---|---|---|
| Redis: question 17 | The question and its answer | Delete the key after the correction commits |
| Redis: the test paper | The full paper with answers | Bump the test's version, so a new key is used |
| Redis: 20,000 result summaries | Each student's score card | Bump a version in their keys; don't delete 20,000 keys one by one |
| Materialized view: leaderboard | Ranks for every student | Refresh after re-scoring finishes |
| Redis: leaderboard JSON | Top 100, cached for 30 seconds | Let the TTL expire after the refresh |
| CDN and Nginx: public results page | The toppers list | Purge by tag or prefix at the CDN; Nginx's one-second microcache expires by itself |
| CDN and browsers: solutions PDF | The old answer | Publish 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:
| Data | TTL | Reasoning |
|---|---|---|
| Fingerprinted static files | A year | A new version gets a new URL; nothing is ever invalidated |
| Course catalogue and syllabus | 10 to 60 minutes, plus delete on publish | Changes are planned; brief staleness is harmless |
| Test paper during a live test | The whole test window, pre-warmed | It must not change mid-test; corrections get a new version |
| Leaderboard | 30 to 120 seconds | Students accept "as of a minute ago" |
| Student dashboard summary | 1 to 5 minutes, plus delete on the student's own actions | Their own actions must show at once |
| Live-class viewer count | 5 to 10 seconds | Approximate is fine |
| Feature flags and configuration | 30 to 60 seconds | Short enough that switching something off works quickly |
| Enrolment and access rights | Very short or none, plus delete on change | A refund or a ban must take effect |
| Fees, payments and order status | None, or a few seconds | Money 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:
- 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.
- 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.
- 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
| Invalidation | Expiry | Eviction | |
|---|---|---|---|
| Why the entry goes | The data changed | Its TTL ran out | The cache needed memory |
| Who decides | Your code or a purge | The clock | The cache's eviction policy, such as LRU or LFU |
| What it protects | Correctness | A limit on staleness | Capacity |
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:
- Render the response from the write. After saving, show the page using the data you just saved, not a fresh cached read.
- Update the cache as part of the write for the entity the user just edited, which is write-through for that one key.
- 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.
- 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.