0%
Redis Deep Dive: Encodings, Memory, and Failover
The Server Model
Data Structures Inside
Memory Management
Persistence
Replication and Cluster
Caching Patterns, Stampedes and Hot Keys
There are about five ways to put a cache in front of a datastore, they have been given names, and the names matter less than the two questions each one answers differently: who writes to the cache, and what happens when the cache and the record disagree.
Cache-aside, the default
The application looks in the cache. On a miss it reads the database, writes the value into the cache with a TTL, and returns it. On a write it updates the database and deletes the cache entry.
This is the pattern the overwhelming majority of systems use and it is the right default. The cache is optional: if Redis is down, every request becomes a database read and the system is slower rather than broken. The application owns all the logic, which makes it explicit and easy to reason about, and the cost is that the logic appears at every call site unless it is wrapped.
Read-through, the same thing moved
The application asks the cache, and the cache itself fetches from the database on a miss. The difference from cache-aside is only where the fetch logic lives: inside a library or a proxy rather than in the calling code.
Redis does not do this natively, so read-through in a Redis system means a client-side library that wraps the miss path. That is a genuine improvement in consistency of implementation, since every caller gets the same TTL policy and the same miss handling, and it introduces a dependency on the cache being reachable for the fetch to be attempted at all.
Write-through
Every write goes to the cache and the database synchronously, in that order or the reverse, and the write is not acknowledged until both have accepted it. The cache is therefore never stale for a key that was written through it.
The cost is latency on every write and a new failure mode: if the cache write succeeds and the database write fails, or the reverse, the two disagree and there is no transaction spanning them. Most implementations handle this by writing the database first and treating a cache failure as a reason to delete rather than to retry, which quietly turns write-through into cache-aside.
Write-behind
Writes go to the cache and are flushed to the database asynchronously, in batches. Throughput is excellent because the slow store is written in bulk and the caller waits for neither.
This is the pattern to be careful with, because the cache now holds the only copy of recent writes and Redis is not a durable store by default. A crash loses whatever had not been flushed, and the amount lost depends on the flush interval rather than on anything the application controls. It is appropriate for metrics, counters and other data where losing a few seconds is acceptable, and inappropriate for anything a user would notice.
Cache as the record
Some data has no other home: a session, a rate-limit window, a short-lived token. Here Redis is the database rather than a cache, and the vocabulary of hit rates and invalidation does not apply.
It is worth separating this case explicitly because it changes every operational decision. There is no miss path to fall back to, so availability of Redis becomes availability of the feature. Eviction must be configured so these keys cannot be evicted, persistence has to be considered on its own terms, and the capacity question becomes a hard limit rather than a tuning exercise.
What the TTL is actually for
A TTL bounds staleness. It also bounds memory, caps the damage from a missed invalidation, and spreads load by ensuring entries are refreshed at different times. Those are four separate jobs and a single number does all of them.
This is why TTL choice is less arbitrary than it looks. If invalidation is reliable, the TTL is a safety net and can be long. If invalidation is unreliable or absent, the TTL is the entire consistency story and must be short enough that the staleness is acceptable. Working out which of those situations you are in is the first thing to establish about any cache.
Deleting versus updating on a write
On a write, the application can delete the cache entry or overwrite it with the new value. Deleting is almost always correct, and overwriting is the source of a specific and nasty class of bug.
Overwriting requires that the value being written is the complete, current value, which it often is not: concurrent writers can overwrite each other out of order, and a value derived from a partial update is simply wrong. Deleting means the next reader recomputes from the source of truth, which costs one database read and is correct regardless of ordering. The cost of correctness here is one miss.
Caching negative results
A lookup that finds nothing is a result too, and not caching it means every request for a nonexistent key reaches the database. For keys that are requested repeatedly and never exist, which includes most scanning and probing traffic, that is the whole load.
Negative caching with a short TTL fixes it cheaply. The TTL should be much shorter than for positive entries, because the cost of serving a stale miss for something that now exists is usually higher than the cost of serving a stale hit. A few seconds is often enough, since the traffic being absorbed is repetitive by nature.
What to cache, and what not to
Cache things that are expensive to compute, read far more than written, and tolerable slightly stale. Do not cache things that are cheap to fetch, written as often as read, or where staleness has consequences nobody has thought through.
The middle criterion is the one most often skipped. A cache in front of data that is written as often as it is read does almost nothing: every write invalidates, every read misses, and the system has paid for an extra network hop and a consistency problem in exchange for nothing. Measuring the read-to-write ratio before adding a cache takes one query and settles the question.
Key naming, which matters more than it should
A key should encode everything the value depends on: the entity, its identifier, the version of the serialisation format, and any parameter that changes the result. Anything omitted becomes a correctness bug the first time it varies.
The serialisation version is the one people learn the hard way. Deploy a change to the shape of a cached object without changing the key, and the new code reads old-format values and fails in whatever way the deserialiser fails. A version segment in the key makes a format change a cold cache rather than an incident, and a cold cache is a known, survivable event.
What to take from the patterns
Three things. Cache-aside with delete-on-write is the right default, because the cache stays optional and the logic is explicit. The TTL does four jobs at once, and whether it is a safety net or the entire consistency story depends on whether invalidation is reliable. And a cache in front of data written as often as it is read buys nothing, which is worth checking with a query before building anything.
Delete on write rather than overwriting. Overwriting assumes the value you hold is the complete current one, and concurrent writers can apply their overwrites out of order while a value derived from a partial update is simply wrong. Deleting costs exactly one database read on the next request and is correct regardless of ordering, which is a very good price for removing an entire class of bug.