Caching System Design

Redis Caching: Patterns for Real Workloads

Redis caching in practice: cache-aside with a TTL on every write, the redis TTL strategy, and the stampede, avalanche, and penetration counters implemented for real, part 4 of the Caching in System Design series.

Executive Summary: Redis caching is the practice of implementing the patterns from parts 1-3 with the tool most fleets actually use; an in-memory data server whose capability list is as much risk as feature. This article covers when to use redis as a cache; the contract that keeps the tool a cache instead of a quietly load-bearing database, the cache aside pattern redis spelling: GET, miss, read, SET, with an expiry on every write, and the redis ttl strategy: expiry as garbage collector and staleness bound, jittered against avalanches, plus the part 3 counters implemented for real: stampede locks, probabilistic early expiry, negative caching, and the hot-key reality no server feature fixes.

This article arrives with debts recorded across the series. Part 1 drew the patterns (caching in system design) with boxes and arrows and left the tool blank on purpose. Part 2 (distributed caching) made the fleet real, compared redis with memcached, and promised that the Redis-specific patterns live here, with the internals (data structures, persistence, Sentinel vs Cluster) reserved for part 5. Part 3 (cache stampede and failure modes) catalogued the failure taxonomy and named this article as the countermeasure: the same patterns and counters, implemented with the standard tool. This is that implementation, and it begins where part 2 ended, with the warning that a cache server which can do everything gets asked to do everything, and the contract that keeps it from saying yes.

Redis caching is the use of Redis (an in-memory data server) as the disposable copy in front of a slower source of truth: the application checks Redis before reading the database and updates or expires the cached copy on writes. The value is one network round trip against a disk-bound read, a trade that pays for itself on latency budgets measured in milliseconds, and only when the patterns keep the copy disposable.

The contract: Redis as a cache, not a database

The contract from part 1 survives the tool unchanged: Redis holds copies, the database holds truth, and every key must be flushable without losing anything a cold start cannot explain. What the tool adds is temptation. Redis ships with persistence, replication, sorted sets, streams, scripting, and even distributed locks; a feature list that makes it easy to drift from cache to datastore one convenience at a time. Part 2 called this the central risk of choosing Redis at all: the more a cache server can do, the more it gets asked to do things a cache must not; become state that cannot be flushed, a component whose loss reduces availability instead of speed. Every pattern in this article exists inside that contract.

When to use Redis as a cache is decided by the workload before the tool. The pattern pays when reads repeat, when the source of truth is slower than memory by a wide margin, and when the data tolerates a staleness window measured in seconds or minutes; feed reads, catalogue lookups, computed results, deduplicated third-party calls. It does not pay for write-heavy data, because a cache accelerates reads and write-through doubles the write path; nor for strictly fresh reads (balances, inventory, authorization) where a stale window is a correctness bug rather than a trade; nor for one-pass traffic with no hit ratio to harvest. And the same part 1 discipline applies before any of it: fix the query first, an index composes with caching; a cache in front of a broken query hides it.

Three Redis facts shape every pattern that follows. First, the dataset lives in memory under a configurable ceiling: Redis caching is memory budgeting, and keys written without expiry accumulate until the ceiling forces a choice; the eviction policies, and which one to pick, are part 5’s subject. Second, single-key commands are atomic; increments, set-if-absent, get-and-delete are safe without application locks, which is exactly what the stampede counters need. Third, Redis speaks pub/sub, which is how a fleet spreads an invalidation, with all the delivery-failure questions part 2 attached to that channel.

The read-and-write patterns this article implements are part 1’s; the table recaps each definition in one line and adds the Redis-specific note that changes it in practice.

PatternDefinition in one lineStaleness windowWrite costRedis note
Cache-asideApplication reads on a miss and writes the copy backUp to the TTLUnchanged; database onlyThe default this article implements
Read-throughClient library fills misses transparentlyLibrary-managedUnchangedBehavior lives in a shared library, not in each service
Write-throughCache updated on every writeNear zero for cached keysDoubled; cache and databaseA second SET after the commit
Write-behindCache accepts writes and flushes laterFresh reads, durability deferredDeferred and batchedTurns the cache into a store; the contract’s violation

Cache-aside in Redis

The cache aside pattern in Redis is four commands and one rule. The application asks Redis for the key; a hit serves in one round trip and the database never hears the request. A miss reads the source of truth, writes the copy back, and serves it, and the write-back always carries an expiry, so a key the application forgets to invalidate deletes itself instead of aging into stale state. In commands:

  1. GET user:42, the hot path. One network hop, and on a hit the read is done.
  2. On nil, read the source of truth: the database row, the remote API, the computed value.
  3. SET user:42 (serialized value) EX 900, store the copy with an expiry. The EX is not decoration; a SET without it creates state with no defined end.
  4. Serve the value. The next reader repeats step one and hits.

The pattern’s virtues survive the translation intact. The database remains the truth, so a flushed or crashed cache degrades the system to its uncached speed rather than breaking it: fail-open, the stance part 3 frames. The application owns what gets cached, so features nobody reads cost nothing. And the same four steps cover every read shape: an entity under one key, a collection under a list, a ranked feed under a sorted set, a counter under INCR. What Redis adds over the textbook boxes is the atomic single-key toolkit (SET with NX becomes a lock, INCR becomes a window) and the counters later in this article are built from exactly those primitives.

The implementation choices decide whether the pattern stays cheap. Key names should describe the thing cached (user:42, feed:home:42) because key discipline is what makes a later invalidation targetable instead of fleet-wide. Values should cache the response shape the caller needs, serialized small enough to ship in one packet, rather than the raw row the database happened to store. Batching keeps the round-trip count flat: MGET fetches a page of keys in one hop, the way part 2’s routing fetched them from one node. And the client library deserves the same scrutiny as the server: in cache-aside it is a co-author, and its pooling, pipelining, and behavior when Redis is slow or gone are production behavior, not details.

Write paths: keeping copies honest

Cache-aside says nothing about writes, and the silence is itself a decision: the cached copy ages out at its TTL. The deliberate options all hook the write path. Write-through updates the copy in the same request that commits to the database: a second SET, with the same expiry discipline. Invalidation deletes instead: DEL after the commit, and the next read repopulates. Write-behind accepts the write into Redis and flushes to the database later: possible, occasionally right, and a quiet conversion of a disposable copy into a durability dependency, with the write-back price list from part 1 attached. Most workloads want the first two and should say so in a design document rather than discover it in an incident review.

Between update and delete, delete is the safer default, and the ordering rule matters more than the choice: the invalidation runs after the database commit, in the code path that knows the commit succeeded. An update or delete issued before the commit can interleave with a concurrent write and resurrect the stale value that write just replaced (the read-your-own-writes problem part 1 flagged) and a TTL only bounds the damage; it does not prevent it. What a fleet adds to the picture is delivery: the delete has to reach every node holding a copy, and Redis pub/sub is the mechanism most teams reach for, inheriting exactly the what-happens-when-the-message-is-lost question that made cross-node invalidation hard in part 2. The full taxonomy of deliberate techniques (purges, versioned keys, the write-through guarantee) is part 6’s subject, and this article deliberately stops at the hook.

TTL strategy: expiry as the safety net

A Redis TTL strategy answers one question per key: how wrong may this copy be, and for how long? The expiry is the cache’s garbage collector and its staleness bound at once. Part 1’s rule carries over unchanged (every key gets a TTL) but Redis makes the rule load-bearing: keys written without EX do not age out; they accumulate, until the memory ceiling forces the server to choose victims, and the choice of eviction policy (allkeys-lru, volatile-lru, and their relatives) is a sharp-edged question part 5 examines. Pattern-level guidance is enough here: treat memory as the budget, treat TTLs as the recurring cost, and never design for the ceiling to do the accounting.

The number itself is per data class, not global. A stable catalogue tolerates hours; a feed tolerates minutes; a rate-limit window tolerates exactly its window; a session tolerates its idle timeout. Two refinements matter at fleet scale. Jitter: identical TTLs written by the same deployment expire in the same instant, and synchronized expiry is the avalanche of part 3; a fleet-wide EX 900 is a fleet-wide cold start fifteen minutes later, so expiries get a random spread across the interval. And negative caching: a miss you decide not to cache is a miss you will keep serving from the database; caching the absence, with a short sentinel TTL, is a TTL decision before it is a penetration counter.

What a TTL cannot deliver is freshness on demand. When a write must be visible to the next read, the TTL bounds the blast radius of a missed invalidation but does not remove the need for one; that is the fast path from part 2, implemented in Redis as a targeted delete or a pub/sub event, and catalogued in part 6. The shape of the advice is the shape of the whole series: TTLs make invalidation optional where data is slow and mandatory where it is fast, and no amount of server speed changes which side of that line a key falls on.

The part 3 counters, in Redis

Part 3 catalogued the three ways a cache fails under load and pointed here for the implementations. The counters turn out to be small compositions of the atomic commands already on the table, which is the argument for having them on the table.

  • Stampede, the thundering miss. A popular key expires, and every request that would have hit it misses together, each one starting its own rebuild. The counter collapses them: the first miss takes a lock (SET lock:user:42 1 NX EX 10) rebuilds, and repopulates the key; concurrent misses see the lock and either wait briefly or serve the previous value as stale-while-revalidate. One rebuild instead of a thousand, and a database that sees one query.
  • Probabilistic early expiry. A refinement that removes even the lock: as a key nears expiry, each hit grows more likely to refresh it early, so some request recomputes just before the fleet ever observes the miss. The pattern is a few lines in the client library, and it trades a small amount of redundant work for the disappearance of synchronized rebuild cost.
  • Avalanche, synchronized expiry at fleet scale. The counter is the jitter from the TTL section, applied honestly: no shared TTL constants across a deployment, no warm-up routine that fills a fleet with identical expiries, and a cold-start plan for the failover that resets the fleet anyway, whether to fail open or closed is part 3’s question, not this article’s.
  • Penetration; requests for keys that do not exist. Uncacheable misses spend database budget forever, and attackers know it. Redis counters it two ways: negative caching (store an absent sentinel with a short TTL) and, for high-volume namespaces, a bloom filter in front of the cache that answers definitely-absent cheaply and keeps nonexistent keys from ever reaching the expensive layers.
  • Hot keys, the one-node celebrity. Placement is not a Redis feature: a key that dominates traffic lands on exactly one node no matter how large the fleet, because consistent hashing sends it there. The counters are part 2’s, and they translate directly: split the key into suffixed variants, replicate the read, or absorb it in a small in-process tier in front of Redis.

None of the counters changes the contract, which is why they belong in a caching article rather than an infrastructure one: a stampede lock is not state, a bloom filter is not state, and an absent sentinel is not state; each is disposable, bounded, and flushable. The failure to keep them that way is not a Redis failure; it is the drift this article opened with, arriving one convenience at a time.

FAQ

When should you use Redis as a cache?

When reads repeat, the source of truth is slower than memory by a wide margin, and the data tolerates seconds to minutes of staleness; feed reads, catalogue lookups, computed results, deduplicated third-party calls. Not when writes dominate, when reads must be strictly fresh, or when the dataset already fits in the database’s own memory: the tool changes the implementation, not the decision.

How does the cache-aside pattern work in Redis?

GET on the hot path; on nil, read the database, SET the serialized copy with an expiry, and serve. The application owns population and invalidation, the database remains the truth, and a dead Redis degrades the system to its uncached speed rather than breaking it. The one rule with no exception: every SET carries a TTL, so the pattern stays safe to forget.

What TTL should Redis cache keys have?

The shortest one the data class tolerates, jittered at fleet scale so a deployment does not expire together. Stable data takes hours, volatile data takes minutes, rate windows take exactly their window, and absent results take a short sentinel expiry. A key that cannot tolerate its TTL does not need a bigger number; it needs invalidation, the fast path catalogued in part 6.

Should writes update the cache or invalidate it?

Invalidate by default: delete the key after the database commit and let the next read repopulate. Updating the copy is reserved for keys hot enough to pay the double write and fresh enough to need it. Either way the hook runs after the commit, before it, a race can resurrect the stale value the write just replaced. Write-behind belongs only where a durable buffer and a flush design already exist.

Is Redis a cache or a database?

It is a data server that plays whichever role the design assigns. As a cache it is excellent and disposable; as a store it needs persistence, replication, and failover treated as first-class requirements; the machinery part 5 covers. The decision is architectural, and pretending it is a configuration setting is how caches quietly become systems.

C-004 system-design

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *