How Redis Works as a Cache - and When It Betrays You
Redis is fast until it isn't. Here's how Redis actually works as a cache - including eviction policies, cache stampede, and the failure modes most engineers miss.
On this page
How Redis Works as a Cache - and When It Betrays You
Most backend engineers have set up Redis at some point. You add it between your app and your database, keys go in, reads get faster, and you move on. But a few months later something breaks in production at 2am, and you realise you never actually understood what Redis was doing under the hood.
This is an attempt to fix that.
Why Redis is fast
Redis keeps everything in RAM. That’s the whole reason it’s fast. A database read often involves going to disk, which takes milliseconds. A Redis read takes microseconds. At scale that difference compounds aggressively.
But RAM is expensive and finite, which is what makes Redis interesting. It can’t just hold data forever. It has to make decisions about what to keep and what to throw away - and those decisions have consequences you need to understand.
How caching actually works
When your app queries Redis and finds the data, that’s a cache hit. When it doesn’t, that’s a cache miss, and the request falls through to your database. The database responds, your app writes the result back to Redis with an expiry time (TTL), and the next request finds it there.
This pattern is called cache-aside, and it’s the most common one. You handle the caching logic in your application code. Redis doesn’t know your database exists - it’s just a fast key-value store your app happens to use.
The TTL matters. Set it too short and you’re hammering your database constantly. Set it too long and users see stale data. There’s no universal answer - a user’s profile might survive an hour, but a product’s price might need five minutes.
What happens when Redis runs out of memory
This is where most engineers stop thinking, and where things get interesting.
When Redis hits its memory limit, it has to evict keys to make room for new ones. Which keys it evicts depends on the eviction policy you’ve configured. The default on many managed services is volatile-lru, which only considers keys that have a TTL set. If you haven’t set TTLs on your keys, Redis returns an error instead of evicting anything.
The policies that matter in practice:
allkeys-lru - evicts the least recently used keys across all keys. This is the right default for most caching use cases. If a key hasn’t been touched in a while, Redis assumes it’s not worth keeping.
allkeys-lfu - evicts the least frequently used keys. Better than LRU when you have popular keys that get accessed in bursts but might sit idle between spikes. LFU tracks access frequency, not just recency.
volatile-lru - same as allkeys-lru but only considers keys with a TTL. Keys without TTL are never evicted. Useful when you have a mix of cached data and persistent config stored in the same Redis instance - but mixing those two things in one Redis instance is usually a mistake to begin with.
noeviction - Redis returns an error when memory is full. Sounds bad, but it’s the right choice when Redis is your primary store rather than a cache. You’d rather surface an error than silently lose data.
If you’re running Redis as a pure cache, set allkeys-lru and move on.
Cache stampede - the failure mode that will ruin your day
Imagine a popular product page backed by a Redis cache with a 5-minute TTL. At 2pm, 3000 users hit that page at exactly the same moment the cache key expires. All 3000 requests miss the cache simultaneously. All 3000 requests go straight to your database. Your database buckles.
This is cache stampede. It’s not a theoretical problem - it happens, and when it does it usually brings down your database at exactly the moment you can least afford it, because your cache just expired because your traffic was high.
A few ways to handle it:
Probabilistic early expiration - before the key officially expires, a small percentage of requests will treat it as expired and refresh it proactively. The rest continue serving the cached value. This spreads the refresh load across a time window rather than concentrating it at expiry.
Distributed locks - when a cache miss happens, the first request acquires a Redis lock before querying the database. Other requests wait or serve a slightly stale value. Only one database query fires. The tradeoff is added complexity in your application layer.
Background refresh - a separate process refreshes cache keys before they expire. The cache never technically misses because something is always refreshing it in the background. This works well for data with predictable access patterns but adds operational overhead.
The simplest mitigation for most systems is adding jitter to your TTLs. Instead of every key expiring at exactly T+300 seconds, expire them at T+270 to T+330 seconds. This staggers the expiry windows so you never get a synchronized stampede.
The write-through vs cache-aside debate
Cache-aside (described above) is reactive - the cache only gets populated after a miss. Write-through is proactive - whenever you write to your database, you also write to Redis immediately.
Write-through keeps the cache warm and reduces miss rates. The downside is that you write to Redis even for data that might never be read again, which wastes memory. For frequently read, infrequently written data (product listings, user profiles) write-through makes sense. For high-write, low-read data it doesn’t.
There’s also write-behind (or write-back) where you write to Redis first and let Redis eventually flush to the database asynchronously. This gives you very low write latency but introduces the risk of data loss if Redis goes down before the flush happens. This is rarely the right choice unless you deeply understand the failure implications.
When Redis is not the right tool
Redis is not a database. This sounds obvious but the distinction gets blurry.
If your data is too large to fit in RAM, Redis gets expensive fast. A few gigabytes of cached data is fine. Tens of gigabytes starts to hurt. For truly large datasets, consider a tiered approach - Redis for hot data, a database for the rest. To learn how caches are structured in memory with eviction policies, read our walkthrough on Low Level Design of an In-Memory Cache.
Redis is also a single point of failure unless you set up replication or clustering. In cluster mode, it relies on consistent hashing to distribute data slots across nodes. In standalone mode, if Redis goes down, your entire caching layer disappears and every request hits your database simultaneously. You need to plan for this. Circuit breakers that detect Redis unavailability and route traffic directly to the database (with rate limiting) are standard practice in production systems.
Finally, Redis doesn’t guarantee data durability by default. It’s an in-memory store. Enabling persistence (RDB snapshots or AOF logs) helps but adds latency and disk overhead. If you’re storing anything you genuinely can’t afford to lose, Redis is the wrong place to store it.
The thing most engineers misunderstand
Redis is a tool that makes your reads faster by trading memory for speed and accepting some risk of stale data. It’s not magic and it doesn’t make your system simpler - it makes reads faster while adding a caching layer you now have to reason about, monitor, and maintain.
The engineers who use Redis well are the ones who’ve thought through what happens when it misbehaves: when keys expire simultaneously, when memory fills up, when the instance goes down. The engineers who use it poorly treat it as a black box and discover the failure modes at the worst possible moment.