Redis / Intermediate: application correctness and coordination / Lesson 7
Cache-aside, invalidation and stampedes
Design cache hits, misses and invalidation around an authoritative source and explicit freshness requirements. Analyze race schedules and overload instead of treating TTL as complete consistency.
On this page
Lesson goals
- Explain cache-aside read/write paths and stale-repopulation races.
- Choose bounded fallback, refresh and failure policies.
Name the source of truth and the cached claim
In cache-aside, the application first checks Redis and loads a miss from an authoritative service, then may populate the cache. A hit is only a usable representation under the keyโs version, scope and freshness contract. The authoritative write and Redis invalidation are separate systems unless a specific integration coordinates them. Decide whether stale reads are acceptable, especially for account revocation, inventory or pricing. Redis availability alone does not establish source correctness, and a cache outage should not automatically permit a security-sensitive operation to use unknown state.
Describe the hit/miss protocol before implementing it
The local model returns a cached value only when its version agrees with the version expected by this synthetic caller. Otherwise it loads the supplied authoritative value. This model does not solve how that expected version is learned or distributed; that is a critical consistency requirement in the real design. It illustrates that a hit must be validated against a contract rather than accepted merely because bytes exist. A real implementation needs source timeouts, serialization checks and an explicit response when either the cache or authoritative service is unavailable.
def choose_value(cached, expected_version, authoritative):
if cached is not None and cached['version'] == expected_version:
return cached['value'], 'hit'
return authoritative, 'source'
assert choose_value({'version': 2, 'value': 'current'}, 2, 'source') == ('current', 'hit')
assert choose_value({'version': 1, 'value': 'old'}, 2, 'new') == ('new', 'source')
The assertions validate only a supplied-version decision. They make no claim about distributed invalidation or a live Redis hit rate.
An invalidation can precede a late stale fill
Consider a reader loading version one while a writer commits version two and deletes the cache key. If the old reader fills Redis afterward, the cache becomes stale again despite successful invalidation. Deleting after the source write is useful but is not a proof against every ordering. Versioned values, generation keys, revalidation and coordinated event delivery can address particular protocols, each with assumptions that must be tested. Do not promise strong consistency from an arbitrary delay or a second delete. Record the permitted staleness and recovery behavior explicitly.
Make the stale-fill schedule visible offline
This model fixes the schedule that can defeat naive invalidate-after-write. The final cache contains the earlier source value even though the authoritative state has advanced. No Redis or source database is used. The model is a counterexample, not a concurrency simulator or recommended fix. A reviewed design should state how a fill proves it is still eligible, how invalidation events are retried and ordered, and how readers handle a detected mismatch. This is especially important when a previously granted capability must be revoked quickly.
source = {'version': 1, 'value': 'old'}
reader_copy = dict(source)
source = {'version': 2, 'value': 'new'}
cache = None # writer invalidates after its authoritative commit
cache = reader_copy # an older read completes late
assert cache['version'] == 1
assert source['version'] == 2
The assertions show one valid race schedule. They do not measure a real service or establish a complete invalidation algorithm.
Bound fallback and synchronized reloads
Many simultaneous misses can overload the authoritative service. Request coalescing, bounded refresh workers, admission control and carefully limited TTL jitter can reduce a stampede under their stated assumptions. Negative caching must distinguish a legitimate absence from source failure and follow revocation and privacy requirements. Serving stale data is an explicit business policy, not a universal availability improvement. Track hit rate together with source load, stale responses, reload failures and eviction. A high hit rate can coexist with wrong data, so it is not a correctness certificate or reason to ignore invalidation lag.
Common errors and fixes
The cache becomes stale after successful invalidation
- Why it happens
- An earlier reader repopulated the key after the writerโs delete.
- How to fix it
- Review fill eligibility, versions and the invalidation delivery protocol.
- Prevent it next time
- Test interleavings rather than only sequential read/write cases.
An outage turns misses into source overload
- Why it happens
- Fallback concurrency and retries were unbounded.
- How to fix it
- Use reviewed admission, coalescing and timeout policies.
- Prevent it next time
- Test cold-start and broad-expiry behavior in an approved offline or isolated model.
A failed source lookup is cached as not found
- Why it happens
- Failure and authoritative absence were conflated.
- How to fix it
- Represent error and absence as distinct outcomes.
- Prevent it next time
- Document negative-cache freshness and retry semantics.
Check your understanding
1. A profile is revoked in the source and the cache key deleted. Why is the revocation guarantee still incomplete?
Show worked solution
An older in-flight read can refill the old version after deletion, and other cached representations may remain. Define a coordinated freshness/version protocol, bounded stale use and invalidation coverage. Verify the authorization decision against that requirement rather than assuming a successful DEL proves every consumer immediately uses the new state.
2. A cache reports a 99% hit rate. Is that sufficient evidence that the caching design is healthy?
Show worked solution
No. Hits can be stale, incorrectly scoped or malformed, while the remaining misses can still overload a slow source. Evaluate correctness, freshness, source latency, invalidation lag, eviction, error handling and resource cost. A performance ratio measures one aspect of service behavior and should be interpreted alongside its population and business contract.
Key takeaways
- Cache-aside requires an authoritative source and explicit consistency limits.
- Test stale fills, negative outcomes and overload as first-class cases.