Backend
Redis as a distributed caching layer
A cache trades consistency for latency, and most caching incidents come from forgetting that trade. This guide covers the patterns, expiry strategy, failure modes and topology choices we consider before putting Redis in front of a database.
· 5 min read · Starone Engineering
Adding Redis in front of a slow query is one of the quickest performance improvements available, and one of the easiest to get wrong. The first version works. The problems appear later: stale data that nobody can explain, a database that falls over when a popular key expires, a cluster that runs out of memory at peak.
Our starting position is that a cache is an optimisation the system must be able to live without. If Redis is flushed, the application should become slower, not incorrect or unavailable. Every decision below follows from holding that line.
Pick the write pattern deliberately
Cache-aside is our default. The application reads from Redis, falls back to the database on a miss, and populates the key. On a write it updates the database and then deletes the key. Deleting is safer than setting the new value, because two concurrent writers can otherwise leave the older value in the cache. A narrow race remains, where a reader loads the old row just before the delete and writes it back just after, which is one reason every key still carries a TTL.
Write-through updates the cache as part of the write path, so reads after a write are warm and consistent with what the application last wrote. It costs a second write on every update and caches data that may never be read. Write-behind acknowledges the write from the cache and persists asynchronously. It gives the lowest write latency and can lose acknowledged data if the node fails, so we restrict it to data that can be lost or rebuilt, such as counters and activity signals.
TTLs, jitter and invalidation
A TTL is a statement about how stale a value is allowed to be, and that is a product question as much as a technical one. A product catalogue may tolerate minutes; a permission check may tolerate seconds or nothing. Explicit invalidation on write shortens the window, but it should supplement expiry rather than replace it, because an invalidation message can always be lost.
Keys written together expire together. If a deployment or a batch job warms ten thousand keys with the same TTL, they all miss in the same second and the database receives the full load at once. Adding random jitter to each TTL, a few per cent of its length, spreads the expiry over time at no cost.
Cache stampede and how to prevent it
A stampede occurs when an expensive, popular key expires and many requests miss simultaneously, each recomputing the same value. The database sees a burst of identical heavy queries at precisely the moment the cache was meant to be protecting it. The mitigations are well understood and can be combined.
- Single-flight: one request acquires a short-lived lock, for example SET with NX and an expiry, and recomputes the value; the others wait briefly or serve the previous value.
- Stale-while-revalidate: keep the value past its logical expiry and refresh it in the background, so no user request pays for recomputation.
- Probabilistic early refresh: each reader has a small, rising chance of refreshing as expiry approaches, which avoids a hard edge without any lock.
- Negative caching: store a short-lived marker for records that do not exist, so repeated lookups for missing identifiers do not reach the database every time.
Hot keys and big keys
Redis executes commands on a single thread per shard, which makes its behaviour predictable and makes two key shapes dangerous. A hot key is read so often that one shard saturates while the rest of the cluster idles; sharding does not help, because a key lives in exactly one slot. The usual answers are a small in-process cache in front of Redis with a TTL of a second or two, or reading from replicas where slightly stale data is acceptable.
A big key, such as a hash or sorted set with millions of members, blocks the event loop when it is read whole or deleted. Commands like KEYS and unbounded range reads have the same effect and should be banned from application code in favour of SCAN and paginated access. Large structures should be split across keys, and removed with UNLINK so memory is reclaimed off the main thread.
Memory limits and eviction policy
A cache without a memory limit will eventually be stopped by the operating system. We set maxmemory explicitly, leave headroom for replication buffers and fragmentation, and choose the eviction policy to match the data.
- allkeys-lru or allkeys-lfu when everything in the instance is a cache entry; LFU copes better when a few keys are much more popular than the rest.
- volatile-lru or volatile-ttl when the instance mixes expiring cache entries with keys that must not be evicted, accepting that writes fail once only non-expiring keys remain.
- noeviction when Redis holds queues, locks or rate-limit state that must not disappear silently; the application then has to handle write errors.
Sentinel or Cluster
Sentinel provides high availability for a single primary with replicas: it monitors the primary and promotes a replica on failure. The data set must fit on one node, and all writes go to one place. For many systems that is enough, and it keeps every multi-key command and transaction available.
Redis Cluster shards data across primaries using 16,384 hash slots and scales writes and memory horizontally. The price is that multi-key operations, transactions and Lua scripts only work when all keys hash to the same slot, which means designing key names with hash tags from the start, and clients must follow redirections during resharding. We choose Cluster when the data set or write rate outgrows one node, not as a default.
A cache is not a database of record
Redis can persist to disk with RDB snapshots and an append-only file, and it can replicate. Replication is asynchronous, so a failover can discard writes that the old primary had already acknowledged, and the default AOF setting flushes once per second. Those properties are sound for a cache and wrong for data that exists nowhere else.
The opposite choice is sometimes right. Session state, rate-limit counters and short-lived locks can live only in Redis when the consequence of losing them is understood: users sign in again, a limit resets, a lock expires. Where losing the data would lose money or violate an invariant, the authoritative copy belongs in a database with durable, synchronous commits, and Redis holds a derived view.
