Redis Cache Invalidation and Hot Keys: A Deep Dive

  • Last Updated: July 20, 2026
  • By: javahandson
  • Series
img

Redis Cache Invalidation and Hot Keys: A Deep Dive

Learn Redis cache invalidation and hot key fixes step by step: TTL and eviction, delete-on-write, SCAN, versioned keys, local caching, and stampede defenses.

1. Introduction

In part one, we looked at how caching works and which patterns to reach for. Now we go one level deeper. This article is a practical guide to Redis cache invalidation and the hot key problem, two things that trip up almost every backend developer the first time they run a cache in production.

Caching sounds simple on paper. You store a copy of some data, you read it fast, and everyone is happy. Then real traffic arrives. Stale data shows up. One popular key melts a single Redis node. A restart wipes the cache and the database falls over. These are not rare edge cases. They are the normal growing pains of a cached system, and they all come back to two questions.

  • When and how do you remove or refresh cached data so users never see stale values?
  • What happens when one key gets far more traffic than the rest?

We will answer both. Along the way we will look at how Redis actually stores and expires data, why invalidation is genuinely hard, and how to spot and fix a hot key before it takes your service down. The examples stay language-neutral so the ideas carry across any stack, though the Redis commands are real.

Diagram showing a read hitting Redis first, a write updating the database and then invalidating the cache key, and a hot key receiving far more traffic than other keys on a single Redis node

2. Why Redis for Caching

Before we talk about invalidation, it helps to know why Redis is the default choice for so many teams. Redis is an in-memory data store. It keeps data in RAM, so reads and writes return in well under a millisecond. That speed is the whole point of a cache.

But raw speed is not the only reason. Redis gives you a few things a plain hash map cannot.

  • Rich data types. Beyond simple strings, Redis supports hashes, lists, sets, sorted sets, and more. You can model real structures, not just key-value pairs.
  • Built-in expiry. Every key can carry a time-to-live. Redis removes it automatically when the timer runs out. This is the backbone of TTL-based invalidation.
  • Atomic operations. Commands like increment run as a single step. Two clients cannot corrupt a counter by racing each other.
  • A shared tier. Many application instances talk to the same Redis. A value cached by one instance is instantly visible to all the others. That fits horizontal scaling perfectly.

Because Redis is a separate process, it survives an application restart. Your cache stays warm even when you deploy new code. That single trait removes a whole class of cold-start problems, though as we will see, it does not remove them all.

2.1 How Redis Stores a Cached Value

A cached entry in Redis is usually just a key and a value with a TTL attached. You design the key as a plain string. Its value is often a serialized object, such as JSON. The commands below show the shape of a typical cache write and read.

# Store a serialized product with a 10 minute expiry
SET product:42 '{"id":42,"name":"Wireless Mouse","price":19}' EX 600
 
# Read it back
GET product:42
 
# Check how long it has left before it expires
TTL product:42   # -> e.g. 512 (seconds remaining)

Notice the key name product:42. A clear naming scheme matters more than it looks. Good keys make it easy to find, group, and invalidate related data later. We will come back to key design when we talk about invalidating many entries at once.

2.2 Choosing the Right Redis Type for a Cache

Most people cache a whole object as one JSON string, and that is a fine default. But Redis offers other types that fit certain caches better. Picking the right one can save memory and make partial updates cheaper.

  • String. The simple choice. Store a serialized blob under one key. Best when you always read and write the whole object at once.
  • Hash. Store an object as a set of fields under one key. You can read or update a single field without touching the rest, which is handy for objects that change one attribute at a time.
  • Sorted set. Great for leaderboards, rankings, or any cache that needs to stay ordered by score. Redis keeps the order for you on every write.

For a first cache, strings are almost always right. Reach for a hash when you find yourself deserializing a large object just to change one field. Reach for a sorted set when order is part of the data itself. Do not overthink this on day one, but know the options exist.

# Same product as a hash: update one field without rewriting all
HSET product:42 name 'Wireless Mouse' price 19 stock 120
HGET product:42 price     # read just the price
HINCRBY product:42 stock -1   # decrement stock atomically
EXPIRE product:42 600     # still attach a TTL to the whole key

3. TTL and How Redis Expires Data

Time-to-live, or TTL, is the simplest form of invalidation. You attach an expiry to a key, and Redis deletes it when the time is up. The data refreshes on the next read because the cache misses and reloads from the database. Many systems run on TTL alone, and for good reason. It is easy to reason about and needs no extra wiring.

But there is a detail people miss. Redis does not watch every key with a stopwatch. That would waste huge amounts of CPU. Instead it uses two mechanisms together.

3.1 Lazy Expiration

When a client requests a key, Redis checks whether it has expired. If it has, Redis deletes it right then and returns nothing. This is cheap because it only happens when someone actually touches the key. The downside is obvious. A key that no one reads can sit expired in memory for a long time, still taking up space.

3.2 Active Expiration

To clean up forgotten keys, Redis also runs a background sampler many times a second. It grabs a small random batch of keys that have a TTL, deletes the expired ones, and repeats if too many in the sample were stale. This keeps memory from filling with dead keys without scanning everything. The two methods together give a good balance of speed and tidiness.

Interview insight
A common question is how Redis expires keys without checking them all constantly. The strong answer names both parts: lazy expiration on access, plus active random sampling in the background. Mention that this is a trade-off. Lazy alone leaks memory, and active alone would be too costly if it scanned every key.

3.3 Eviction When Memory Runs Out

TTL handles time. But what about space? Redis holds everything in RAM, and RAM is finite. When Redis hits its memory limit, it must decide what to drop. This is controlled by the maxmemory-policy setting. The common choices are worth knowing.

PolicyWhat it evictsGood for
noevictionNothing; new writes failCache that must never lose data silently
allkeys-lruLeast recently used key, any keyGeneral-purpose caching
allkeys-lfuLeast frequently used keySkewed traffic with clear favorites
volatile-lruLeast recently used key with a TTL setMixing cache and persistent data
volatile-ttlKey with the nearest expiryPrioritizing short-lived entries

For a pure cache, allkeys-lru or allkeys-lfu is the usual pick. They let Redis shed cold data on its own so hot data stays resident. If you store non-cache data in the same instance, a volatile policy protects the keys that lack a TTL. Choosing the wrong policy is a quiet source of bugs, so set it on purpose rather than leaving the default.

4. The Hard Part: Cache Invalidation

There is an old joke in computer science. There are only two hard things: cache invalidation and naming things. It is a joke, but the first half is true. Invalidation is hard because a cache is a second copy of your data, and now you have to keep two copies in agreement while both change under load.

The goal of Redis cache invalidation is simple to state. When the source data changes, the cached copy must not keep serving the old value for long. How you achieve that is where the difficulty lives. Let us walk through the main strategies from simplest to trickiest.

4.1 TTL-Only Invalidation

The easiest approach is to do nothing special on writes and just let keys expire. You accept that data can be stale for up to the TTL window. For a product description that changes rarely, a five minute or one hour TTL is completely fine. Nobody is harmed by a slightly old value.

This works well when three things are true. The data tolerates some staleness. The TTL is short enough for your needs. And the traffic is high enough that keys get refreshed often. When those hold, TTL-only is the least code and the fewest bugs. Do not over-engineer past it without a reason.

4.2 Write-Through Invalidation: Delete on Change

When staleness is not acceptable, you invalidate on write. The moment the database changes, you also touch the cache. The safest move is to delete the key, not to update it. We saw this in part one, and it matters even more with a shared Redis. The read path below shows the full cycle.

function updateProductPrice(id, newPrice):
    database.update(id, newPrice)      # 1. source of truth first
    redis.del("product:" + id)         # 2. remove the stale copy
    # next read misses, reloads fresh data, repopulates the cache
 
function getProduct(id):
    key = "product:" + id
    cached = redis.get(key)
    if cached is not null:
        return deserialize(cached)      # cache hit
    row = database.findProduct(id)      # cache miss
    redis.set(key, serialize(row), EX=600)
    return row

Why delete instead of update the cache in place? Because two writers can interleave. Writer A reads an old row, writer B writes a new row and updates the cache, then writer A updates the cache with its stale value. Now the cache holds old data while the database holds new data. Deleting sidesteps this. The next read simply misses and pulls the current truth.

Interview insight
If asked “update or delete the cache on write,” answer delete, and explain the race. Updating in place lets a slow writer overwrite the cache with a stale value after a faster writer already fixed it. Deletion is self-healing because the next reader repopulates from the database.

4.3 The Dual-Write Problem

Look again at the update function. It does two things: it writes the database, then it deletes the cache key. What if the process crashes between those two steps? The database is updated, but the stale key still sits in Redis. Every reader now gets old data until the TTL saves you. This is the dual-write problem, and it has no perfect fix, only trade-offs.

Here are the common ways teams reduce the risk.

  • Keep a TTL as a safety net. Even with delete-on-write, give keys a TTL. If a delete is ever missed, the entry still self-corrects within the window. This one habit prevents most permanent staleness.
  • Delete after the commit, not before. Invalidate the cache only once the database write is durably committed. Invalidating first lets a concurrent reader repopulate the cache with the old value before the new one lands.
  • Use change data capture. Advanced setups read the database’s commit log and invalidate cache keys from that stream. Because the log is the source of truth, the cache follows real committed changes rather than hopeful application code.

4.4 Invalidating Many Keys at Once

Sometimes one change affects many cached entries. A user updates their profile, and you have cached ten different views that include their name. How do you clear all of them? This is where key design pays off. If you group related keys under a shared prefix, you can find them together.

It is tempting to reach for the KEYS command to match a pattern. Do not use KEYS in production. It scans the entire keyspace and blocks Redis while it runs, which can freeze every other client. Use SCAN instead. It walks the keyspace in small cursors without blocking.

# WRONG in production: KEYS blocks the whole server
# KEYS user:42:*
 
# RIGHT: SCAN walks the keyspace incrementally, non-blocking
cursor = 0
repeat:
    cursor, batch = redis.scan(cursor, match="user:42:*", count=100)
    for key in batch:
        redis.del(key)
until cursor == 0

An even cleaner design avoids the scan entirely. Store the set of related keys in a Redis set as you create them. To invalidate, read that set and delete its members in one pass, then delete the set itself. This turns a keyspace search into a direct lookup, which is far kinder to a busy server.

4.5 Tag-Based and Versioned Invalidation

Two more patterns solve group invalidation elegantly. The first is versioning. Instead of deleting keys, you bump a version number that is part of the key. Old keys become unreachable and expire on their own later. The second is tagging, where keys carry a tag and you invalidate the tag.

# Versioned keys: bump the version to invalidate everything at once
# Read the current version, then build the real key from it
v = redis.get("ver:catalog")            # e.g. 7
key = "product:" + id + ":v" + v        # product:42:v7
 
# To invalidate the whole catalog view, just increment the version:
redis.incr("ver:catalog")               # now v8; all v7 keys go cold

Versioning is powerful because invalidation becomes a single atomic increment. No scanning, no deleting thousands of keys. The old entries linger until their TTL clears them, using a little extra memory in exchange for instant, safe group invalidation. Many large systems lean on this trick.

4.6 Choosing an Invalidation Strategy

With so many options, how do you pick? The right strategy depends on how fresh the data must be and how many keys one change affects. The table below lines up the choices so you can match one to your case.

StrategyFreshnessBest for
TTL onlyStale up to TTLRarely-changing data that tolerates delay
Delete on writeNear-instantData that must not serve old values
Tracked key setNear-instant, groupOne change clears many related keys
Versioned keysInstant, groupLarge groups; avoids mass deletes
Change data captureFollows commitsStrict correctness at high scale

A practical system often mixes several. You might use versioned keys for a whole catalog, delete-on-write for a single product, and a TTL under all of it as a backstop. Start simple and add a strategy only when a real staleness or scale problem forces it. Layering these on purpose beats guessing.

4.6.1 A Worked Example: A Product Page

Let us trace one realistic case end to end. Imagine a product page that shows the product, its price, and a list of reviews. Each part changes at a different rate, so each part gets its own caching choice.

  • The product details change rarely, so a plain string cache with a one-hour TTL is plenty. Even TTL-only invalidation would be fine here.
  • The price must never look stale, because a wrong price is a real problem. So on any price change, we delete the key immediately and rely on the TTL only as a safety net.
  • The review list grows often but tolerates a short delay. A short TTL of a minute keeps it fresh enough without invalidating on every new review.

Notice how one page uses three different strategies at once. That is normal and healthy. Matching the strategy to each piece of data, rather than forcing one rule everywhere, is what separates a robust cache from a fragile one. It also keeps your invalidation code as simple as each case allows.

5. The Hot Key Problem

Now to the second big topic. A hot key is a single cache entry that receives a wildly disproportionate share of traffic. Think of a celebrity’s profile, a flash-sale product, or the config that every request reads. In a distributed Redis setup, keys are spread across nodes by hashing the key name. That means one hot key lives on exactly one node.

Here is the trouble. When one key gets millions of reads per second, all that load lands on the single node that owns it. The other nodes sit half-idle while that one node saturates its CPU and network. Your cluster looks healthy on average, yet one shard is on fire. This is a hot key problem, and it does not show up until traffic skews.

Diagram of a Redis cluster with three nodes where one node holding a single hot key is overwhelmed with traffic while the other two nodes remain lightly loaded

5.1 How to Detect a Hot Key

You cannot fix what you cannot see. Redis gives you a few ways to find hot keys before they hurt.

  • redis-cli –hotkeys. When the LFU policy is on, this command samples the keyspace and reports the most frequently accessed keys. It is the fastest first look.
  • The MONITOR command, briefly. MONITOR streams every command the server runs. It is heavy, so use it only for a few seconds on a non-critical node, but it reveals which keys dominate.
  • Application metrics. Count cache reads per key name in your own code and ship the top offenders to your dashboards. This is the most sustainable signal because it runs all the time.

Uneven node CPU or network graphs are often the first hint. If one shard is pinned while others idle, suspect a hot key and start sampling. Catching it early turns a page-out incident into a quiet config change.

5.2 Fixing Hot Keys: Local Caching

The most effective fix is to stop sending the traffic to Redis at all. Add a small in-process cache in front of Redis on each application instance. Hot keys get served straight from local memory, so Redis never sees the flood. This is sometimes called a two-tier or near cache.

# Two-tier read: check local memory, then Redis, then the database
function getConfig(key):
    local = localCache.get(key)         # tiny in-process cache
    if local is not null:
        return local                    # hottest path, no network
    value = redis.get(key)
    if value is not null:
        localCache.set(key, value, TTL=5s)   # short local TTL
        return value
    value = database.load(key)
    redis.set(key, value, EX=600)
    localCache.set(key, value, TTL=5s)
    return value

Keep the local TTL short, often just a few seconds. That bounds how stale a local copy can get while still absorbing almost all the read pressure. A key hit a million times a second only reaches Redis a handful of times per instance. The load simply disappears. The cost is a little extra staleness, which is usually a fine trade for surviving the spike.

5.3 Fixing Hot Keys: Key Splitting

Another approach spreads one hot key across many. Instead of a single key, you write several copies with a suffix, then read a random one. Because the copies hash to different nodes, the traffic fans out across the cluster rather than pounding one shard.

# Split one hot key into N replicas across the cluster
# On write, update all replicas
for i in 0..N-1:
    redis.set("hotkey:" + i, value, EX=600)
 
# On read, pick a random replica so load spreads
i = random(0, N-1)
value = redis.get("hotkey:" + i)

Splitting works best for read-heavy, rarely-changing data, since every write must update all copies. For a value that updates often, that write amplification hurts. So reach for splitting when the hot key is mostly read and local caching alone is not enough. Often the two techniques combine well.

Interview insight
Hot keys are a favorite scaling question. A complete answer covers three moves. First, detect with hotkeys sampling or per-key metrics. Second, add a short-lived local cache so most reads never reach Redis. Third, if needed, split the key across nodes to spread the remaining load. Name the trade-off for each: staleness for local caching, write amplification for splitting.

6. The Thundering Herd and Cache Stampede

There is a cousin of the hot key problem that deserves its own section. Picture a very popular key that suddenly expires. In the same instant, thousands of requests miss the cache together. All of them rush to the database at once to rebuild the same value. The database, which the cache was protecting, gets slammed. This is a cache stampede, also called a thundering herd.

It is sneaky because everything looks fine until the exact moment a hot key expires. Then a wall of identical queries hits the database. A few defenses handle it well.

  • Locking on rebuild. Let only the first missing request rebuild the value while the others wait briefly or serve a slightly stale copy. A short-lived Redis lock, set with a NX flag, elects that single rebuilder.
  • Early recomputation. Refresh a hot key a little before it expires, in the background, so it never actually goes cold during traffic. The popular keys stay warm continuously.
  • Jittered TTLs. Add a small random offset to each key’s expiry. Without jitter, keys created together expire together and stampede together. A little randomness spreads the misses across time.
# Lock so only one request rebuilds a hot key on miss
function getWithLock(key):
    value = redis.get(key)
    if value is not null:
        return value
    # SET NX succeeds for only one caller; it becomes the rebuilder
    gotLock = redis.set("lock:" + key, "1", NX=true, EX=5)
    if gotLock:
        value = database.load(key)
        redis.set(key, value, EX=600)
        redis.del("lock:" + key)
        return value
    else:
        sleep(50ms)          # let the winner finish, then retry
        return getWithLock(key)

The lock pattern is the workhorse fix. One request does the expensive work, and everyone else waits a beat and reads the fresh value. Combine it with jittered TTLs and you rarely see a stampede at all. These defenses cost little and save you from a self-inflicted outage on your busiest keys.

7. Cold Starts and Cache Warming

One more failure mode ties the topics together. Suppose Redis restarts, or you deploy a fresh cache. The cache is now empty. Every read misses and hits the database directly. If your traffic is high, that sudden flood can overwhelm the database before the cache refills. A cold cache is a real risk, not a hypothetical.

You have a few ways to soften a cold start.

  • Enable Redis persistence. Redis can save snapshots or an append-only log to disk and reload them on restart. The cache comes back warm instead of empty, so most reads still hit.
  • Warm the cache on deploy. Before sending live traffic to a fresh cache, preload the known-hot keys with a background job. The popular data is ready when users arrive.
  • Ramp traffic gradually. Route a small slice of traffic first so the cache fills before the full load arrives. This gives the cache time to warm without a shock to the database.

Which one you choose depends on how sensitive your database is to a burst. High-traffic systems often use all three. The common thread across this whole article is the same. A cache protects your database, so every plan must ask what happens the moment that protection blinks.

8. Monitoring a Redis Cache

You cannot run a cache well if you cannot see it. A few numbers tell you almost everything about the health of a Redis cache, and they are easy to collect. Watch these and you will catch most problems before users do.

  • Hit ratio. The share of reads served from cache versus reads that fall through to the database. A falling hit ratio means your cache is losing its value, often from too-short TTLs or bad key design.
  • Memory usage and evictions. If Redis is evicting keys constantly, it is too small for your working set, or your policy is wrong. Rising evictions with a falling hit ratio is a clear warning sign.
  • Latency and connection count. A slow Redis is usually a Redis under memory pressure or serving a hot key. Sudden latency spikes on one node point straight at a hot shard.
  • Expired versus evicted keys. Many expirations are healthy TTL churn. Many evictions mean memory pressure. Telling them apart helps you decide whether to grow the cache or shorten TTLs.

The INFO command exposes most of these counters directly. Ship them to your dashboards and set an alert on a dropping hit ratio and on per-node latency. A cache that is watched is a cache that rarely surprises you. This one habit pays for itself the first time it warns you early.

# Pull the key health counters from a running Redis
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses|evicted_keys|expired_keys'
 
# hit ratio = keyspace_hits / (keyspace_hits + keyspace_misses)
redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human'

9. Practical Takeaways

We have covered a lot, so here is the short version you can act on. These habits keep a Redis cache fast, correct, and calm under load.

  • Always set a TTL, even when you also delete on write. It is your safety net against missed invalidations and the dual-write problem.
  • Delete keys on change, never update in place. Deletion is self-healing and dodges the interleaving race between concurrent writers.
  • Design key names with grouping in mind. Prefixes, tag sets, or versions turn painful bulk invalidation into a simple, safe operation.
  • Never run KEYS in production. Use SCAN, or better, track related keys explicitly so you avoid scanning at all.
  • Watch for hot keys with sampling and per-key metrics, then defend with a short local cache and, if needed, key splitting.
  • Guard against stampedes using a rebuild lock, early refresh, and jittered TTLs on your most popular keys.
  • Plan for a cold cache with persistence, warming, and a gradual traffic ramp so a restart never takes down the database.

10. Interview Questions

Q: What is Redis cache invalidation?

A: Cache invalidation is the process of removing or refreshing a cached value once the underlying data changes, so users stop seeing stale data. In Redis this is done three main ways: attaching a TTL so keys expire on their own, deleting the key on every write, or grouping keys under a shared prefix, tag, or version so many entries can be invalidated at once. Most systems combine a TTL as a safety net with delete-on-write for data that must stay fresh.

Q: Should I update or delete the Redis key when data changes?

A: Delete the key. Updating the cache in place opens a race condition where a slow writer overwrites the cache with a stale value after a faster writer already corrected it, leaving the cache behind the database. Deleting is self-healing: the next read simply misses and repopulates from the source of truth. Keep a TTL as well, so a missed delete still corrects itself within the expiry window.

Q: What is a hot key in Redis and how do I fix it?

A: A hot key is a single cache entry that gets a disproportionate share of traffic. Because Redis maps each key to one node by hashing its name, all that load lands on a single shard while the rest of the cluster sits idle. Detect it with redis-cli –hotkeys sampling or per-key metrics, then fix it by adding a short-lived local cache in each app instance so most reads never reach Redis, and if needed splitting the key into several suffixed copies so the traffic spreads across nodes.

Q: How do I prevent a cache stampede when a popular key expires?

A: A stampede, or thundering herd, happens when a hot key expires and thousands of requests miss at once and all rebuild it from the database together. Prevent it by using a short-lived Redis lock set with NX so only the first request rebuilds the value while others wait, refreshing hot keys in the background just before they expire, and adding a small random jitter to TTLs so keys created together do not expire together.

11. Conclusion

Redis makes caching fast, but the speed is only half the story. The real skill is keeping the cache honest and stable once traffic gets serious. Redis cache invalidation is about removing stale data safely, and it always trades some freshness against some complexity. Start with TTLs, add delete-on-write when staleness bites, and keep the TTL as a net for the writes you miss.

Hot keys and stampedes are the load-side twin of that story. One key can overwhelm a node, and one expiry can overwhelm a database. The fixes are cheap and well understood: local caching, key splitting, rebuild locks, and jittered expiries. None of this needs exotic tools. It needs the habit of asking, at every step, what happens when this key changes, gets hot, or disappears. Carry that question into your next project and your cache will protect you instead of surprising you.

Further Reading

Leave a Comment