Redis as a Shared Caching Layer
LESSON
Redis as a Shared Caching Layer
By the end of this lesson, you will be able to...
Explain why a process-local cache stops giving one consistent reuse path when a backend has several instances.
Design a small shared-cache boundary with a source of truth, key, TTL, fallback behavior, and failure assumptions.
Decide when shared Redis state is useful and when a local cache or authoritative read is the better choice.
Idea in one sentence: Redis lets several application instances reuse the same temporary value, but that shared convenience also creates a network dependency and a new failure boundary.
Core Insight
The course API now runs on four instances behind a load balancer.
Each instance has an in-memory cache for GET /courses/204. The cache works well on a quiet development machine. In production, the same popular course page reaches different instances from one request to the next.
Instance A has a warm copy. Instance B has never seen the course. Instance C just restarted. Instance D has a copy that expires two seconds later than A's.
The system still has caches, but it no longer has one shared reuse path.
Redis is useful here because it can hold a temporary copy that all four instances can ask for. It does not replace the course database. It gives the fleet a common place to reuse a value, coordinate its expiration, or maintain a small piece of shared short-lived state.
The important change is not “Redis is fast.” It is:
local cache: one process reuses its own answer
shared cache: many processes can reuse one answer
The Promise We Need to Keep
The product promise is modest:
Public course details should load quickly, and showing a description that is up to one minute old is acceptable.
The promise is not:
Redis always has the latest course record.
The course database remains authoritative. A shared cache is allowed to answer only because the product accepts a bounded amount of staleness for this endpoint.
That distinction protects the next design decisions. The API must name:
- the source of truth: the course database
- the reusable value: the public course-details response
- the shared key:
course:v3:204:details - the trust window: 60 seconds
- the fallback: read the database if Redis misses or cannot answer
- the excluded decisions: enrollment, payment, and authorization still verify authoritative state
Redis gives the fleet a shared copy. The API still owns the policy for using that copy.
The Naive Design: One Cache Per Instance
With local memory, the request path looks like this:
request -> load balancer -> one API instance -> its local cache -> database on miss
Suppose course:204 is popular and the traffic rotates among four instances.
| Instance | Local entry before request | Result |
|---|---|---|
| A | Has version 41 | Fast hit |
| B | Empty | Database read, then local fill |
| C | Empty after restart | Database read, then local fill |
| D | Has a different expiry time | Hit or miss depends on its own clock and history |
This design is not wrong. Local caches have real advantages:
- no network hop
- no separate service to operate
- very low latency for data that stays hot on one instance
- useful protection for repeated work inside a single process
It breaks down when the value must be reused across the fleet. The database still sees several first misses for the same key. Cache behavior now depends on routing, restarts, deployment churn, and which instance happened to serve the previous request.
The naive fix is “put Redis in front of everything.” That also fails. Some values are too personal, too sensitive, too short-lived, or too correctness-critical for shared reuse. A remote lookup can also cost more than a local computation.
The design question is narrower:
Does this value need to be reused or coordinated by more than one instance, and can the system safely tolerate its shared-cache failure behavior?
A Better Boundary: Shared Copy, Authoritative Source
Plain meaning:
A shared cache lets many application instances look at the same temporary answer.
In this scenario:
All four API instances use course:v3:204:details to find one reusable course-details response.
Technical name:
Redis is a shared caching layer. It is a networked component between the API fleet and the source of truth. Its common operations make temporary values, expiry, and small atomic state available across process boundaries.
The boundary should remain explicit:
course database: owns the latest course details
Redis: stores a temporary reusable copy
API: decides when the copy is acceptable and what to do on failure
This means the API does not silently treat Redis as a database replacement. A cache entry may be missing, expired, evicted, stale, or unavailable. The source-of-truth rule survives all of those states.
A Worked Fleet-Wide Trace
Start with four API instances and an empty Redis key:
database: course 204, version 41
Redis key course:v3:204:details: empty
API instances: A, B, C, D
TTL: 60 seconds
Now trace the same public course request through the fleet.
| Step | Request or event | Redis state before | Action | Result |
|---|---|---|---|---|
| 1 | Learner A reaches API instance A | Key missing | A reads database version 41 | A stores version 41 with TTL 60s and returns it |
| 2 | Learner B reaches instance C | Version 41, trusted | C reads Redis | C returns version 41 without a database read |
| 3 | Learner C reaches instance B | Version 41, trusted | B reads Redis | B returns version 41 without a database read |
| 4 | Instance A restarts | Version 41, trusted | Local memory is lost | Fleet still has the shared Redis entry |
| 5 | Learner D reaches new instance A | Version 41, trusted | A reads Redis | A returns version 41 without warming local memory first |
| 6 | Editor updates the course | Version 41 may still exist | Database becomes version 42 | A later lesson chooses invalidation or refresh behavior |
The key transition is step 4. A process-local cache disappears with the process. The shared cache remains available to the other instances and to a replacement instance.
The naive failure contrast is equally important:
naive claim: Redis makes every request fast
better claim: Redis lets fleet-wide requests reuse one acceptable copy while Redis and the source remain within their failure budgets
So far, Redis has solved one problem: fragmented reuse across instances. It has not solved freshness after writes, synchronized cache fills, source overload during a Redis outage, or Redis persistence. Those are separate decisions with separate mechanisms.
Choosing What Belongs in Shared State
The same Redis service can support several small shared behaviors. They should not all use the same key shape or failure contract.
| Need | Example | Useful shared operation | Important boundary |
|---|---|---|---|
| Reuse a response | Public course details | Get/set a value with expiry | The response may be stale within its policy |
| Count a shared event | Requests from one account | Atomic increment with expiry | The counter is a control signal, not a billing ledger |
| Mark short-lived work | “This import is already running” | Set a marker with expiry | Expiry must not allow two unsafe workers to proceed |
| Share an authorization decision | “May this user transfer money?” | Usually avoid a broad shared cache | The authoritative check may be required each time |
The data structure follows the access pattern. A full course response is one value that is fetched and replaced. A counter needs an atomic increment. A short-lived marker needs an expiry rule and a clear owner.
Do not turn this into a catalog of Redis commands. The design move comes first: state the job, its owner, its lifetime, and the consequence if the shared layer is wrong or unavailable. The command only implements that decision.
Key Design and Expiration Are Part of the Boundary
Shared keys need names that state what they mean.
Compare these two keys:
course:204
course:v3:204:details
The first key is vague. Is it a whole course record, a rendered response, a counter, or an old schema? The second key says that it stores version-3 course details. It gives the team a safer way to change the representation later: new code can read v4 without confusing its value with v3.
Expiration also has meaning. A 60-second TTL says the application will reuse the value for up to roughly one minute before fetching again. It is not merely memory cleanup.
One small implementation shape makes the policy visible:
def get_public_course(redis_client, database, course_id):
key = f"course:v3:{course_id}:details"
cached = redis_client.get(key)
if cached is not None:
return cached
current = database.fetch_public_course(course_id)
redis_client.set(key, current, ex=60)
return current
This is cache-aside again, but the copy is now shared. The next question is not “does this code use Redis?” It is “what happens if several instances execute the miss path at the same time?” That is the beginning of stampede control, which the track addresses later.
Trade-offs and Failure Boundaries
The central trade-off is fleet-wide reuse versus a new remote dependency. Redis improves fleet-wide reuse and can simplify small shared operations. It costs a network hop, connection management, memory capacity, key discipline, and failure planning.
A Redis miss is normal
A miss is not a Redis failure. It means the key is absent or expired, so the API follows its source-read path. The source must be able to handle the expected miss rate.
A Redis outage changes the load shape
If every API instance bypasses Redis at once, the database may receive the full request volume. A fallback preserves correctness only if the source has enough headroom. Otherwise, a short Redis incident can become a broader outage.
Useful signals include Redis error rate and latency, cache hit ratio, database query rate, connection-pool saturation, and request tail latency.
Shared does not mean globally consistent
All instances can see the same Redis entry, but the entry can still be old relative to the database. Shared visibility solves fragmented reuse; it does not remove the freshness policy introduced in the previous lesson.
Temporary does not mean harmless
An expiring counter or marker can control user-visible behavior. If it disappears early, is written under the wrong key, or is unavailable, the system may admit too much work or repeat a side effect. Treat short-lived state as behavior, not as disposable clutter.
This helps when many instances need one reusable value or one small shared control. It does not replace the authoritative database, guarantee a fresh value after every write, or make a multi-step workflow atomic. The boundary appears when Redis latency rises, key memory approaches limits, fallback load grows, or a cached answer violates the user promise.
Common Confusions
Confusion: Redis replaces the source of truth
Why it is tempting:
Redis often returns values much faster than the database, and the API reads it first.
Better model:
For a cache, Redis holds a policy-governed copy. The database still decides the current authoritative course record. Some specialized Redis uses have different durability goals, but this shared-cache boundary does not.
Confusion: A shared cache is always better than a local cache
Why it is tempting:
Sharing avoids duplicate fills across instances.
Better model:
Local memory is cheaper and faster for process-private reuse. Use a shared cache when the benefit of fleet-wide reuse or coordination exceeds the network and operational cost.
Confusion: An atomic Redis command makes a whole workflow atomic
Why it is tempting:
An increment or set operation can be atomic inside Redis.
Better model:
That atomicity covers the Redis operation, not a larger workflow involving a database, payment provider, or external message. Name the exact boundary before claiming safety.
Check Your Understanding
Check: Instance A warms course:v3:204:details, then it restarts. Why can instance C still serve a hit?
Think first, then reveal.
Answer: The reusable copy lives in Redis rather than in A's process memory. C can ask Redis for the same key. This improves fleet-wide reuse, but it does not make the value authoritative or permanently available.
Check: A team caches seat_available in Redis for one minute and accepts enrollment using only that cached number. What is the missing design step?
Think first, then reveal.
Answer: The enrollment action needs an authoritative concurrency check. A shared cache may support display or load reduction, but it cannot safely decide the last available seat without a stronger source-of-truth rule.
Practice
Your API fleet has eight instances. For each case, choose local cache, shared Redis cache, or authoritative read, then state the reason and a failure behavior.
- A parsed configuration file is identical for every request on one instance and reloads only when that instance restarts.
- A public product summary is requested by every instance, changes a few times per day, and may be 30 seconds old.
- A checkout service must decide whether a coupon is still valid while another checkout may consume its final use.
- A rate limit needs one request count across all API instances for a 60-second window.
A good answer should mention:
- Local cache is a good fit for the process-private configuration parse because sharing it adds no useful fleet-wide behavior.
- Shared Redis is a good fit for the public product summary when the freshness promise and source fallback are explicit.
- Coupon consumption needs an authoritative concurrency boundary; a broad cache hit cannot decide the final use safely.
- A shared atomic counter with expiry can support the rate-limit control, but the design still needs key scope, outage behavior, and a policy for what happens when the counter cannot be read.
Connections
The previous lesson established that a cache is a non-authoritative copy plus a policy. This lesson moves that copy across process boundaries.
The next lesson moves reuse outward again: browser and CDN caches can satisfy requests before an API instance runs. The same questions remain: who may reuse the value, how long may they trust it, and what happens after the source changes?
Resources
- [DOC] Redis Caching — Focus: See cache-aside and expiration in a shared-cache setting.
- [DOC] Redis Data Types — Focus: Match a value, counter, or short-lived marker to the operation it needs.
- [DOC] EXPIRE — Focus: Treat expiry as part of the behavior contract, not only memory cleanup.
- [BOOK] Designing Data-Intensive Applications — Focus: Connect shared temporary copies to broader storage, replication, and consistency trade-offs.
Key Takeaways
- Redis is useful when several application instances need to reuse one temporary value or coordinate one small piece of short-lived state.
- The source of truth remains outside the cache boundary; the API owns the policy for keys, freshness, fallback, and acceptable failure behavior.
- Shared cache keys and TTLs are behavior contracts. They should state what the value is, who can use it, and how long it remains acceptable.
- Redis improves fleet-wide reuse, but it adds network latency and an operational dependency whose outage can shift load to the source.
- Atomic Redis operations protect one Redis operation, not every surrounding database or business workflow.
← Back to Caching, Workers, and Performance