Redis as a Shared Caching Layer

LESSON

Caching, Workers, and Performance

002 30 min intermediate

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:

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:

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.

  1. A parsed configuration file is identical for every request on one instance and reloads only when that instance restarts.
  2. A public product summary is requested by every instance, changes a few times per day, and may be 30 seconds old.
  3. A checkout service must decide whether a coupon is still valid while another checkout may consume its final use.
  4. A rate limit needs one request count across all API instances for a 60-second window.

A good answer should mention:

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

Key Takeaways

PREVIOUS Cache Fundamentals and Core Patterns NEXT CDN and HTTP Caching Layers