A cache is disposable by design
Redis can make repeated reads fast, but the authoritative value should live somewhere that can be rebuilt. Start by identifying the source of truth and the consequences of a stale or missing cache entry. A cached feature flag, permission, price, and documentation page have different correctness requirements. If the application cannot recover when Redis is empty, the cache has quietly become a database and needs a database-level durability and backup plan.
Define the cache entry's key, value shape, owner, TTL, invalidation path, and failure behavior before writing code. Include a schema version in the key or value so a deployment can safely invalidate incompatible entries. Never let a user-controlled string create unbounded key space. Normalize identifiers, limit value size, and apply a namespace that distinguishes environments and tenants.
Use cache-aside for a clear ownership model
Cache-aside reads from Redis first, loads the source on a miss, then writes the result to Redis. The application owns the decision and can fall back to the source when Redis is unavailable. It is simple, but concurrent misses can all load the same expensive record. Add a short lock or request coalescing for hot keys, and always make the source query safe to repeat.
On writes, update the source first and invalidate or refresh the cache after the transaction commits. Updating the cache before the source creates a window where a later source failure leaves a value that never existed. Deleting after commit can briefly produce a miss, which is usually safer than serving an uncommitted representation. If a write affects many derived keys, publish an invalidation event rather than trying to enumerate every view in the request.
Choose TTLs from business freshness
A TTL is not merely a memory-management setting. It is the maximum time a cache may serve a value without a refresh if no explicit invalidation occurs. Choose it from the business tolerance for staleness and the expected update rate. Add jitter to expiration so a large group of keys created together does not expire at the same instant. Do not extend a TTL to hide a slow source without reviewing the freshness promise.
Some values should not have a long TTL at all. Authorization and billing state often require a server-side check or a short cache with an immediate invalidation event. If a cached value controls access, fail closed when the cache and source cannot be reconciled. A stale marketing label may be acceptable during an outage; a stale entitlement may be a security or revenue incident.
Prevent stampedes and hot-key overload
When a popular key expires, many requests can miss and load the same source simultaneously. Use a single-flight pattern inside one instance, a short distributed lock, or stale-while-revalidate where the product can tolerate it. Set a lock expiry so a crashed loader cannot block refresh forever. The lock should coordinate work, not become the authoritative state. A second request that cannot obtain the lock should wait briefly, serve a safe stale value, or return a bounded temporary error.
Hot keys can also overload one Redis shard even when overall traffic is low. Measure key frequency, command latency, evictions, and memory fragmentation. Avoid storing enormous lists under one key when a sorted set or paginated structure can distribute work. If a key is intentionally hot, precompute it, replicate it through the supported mechanism, or put a local short-lived cache in front of Redis with an explicit staleness budget.
Treat serialization and failure as contracts
Use a versioned, bounded serialization format and validate values when reading. A malformed or old cache entry should be ignored and rebuilt, not passed into business logic as if it were authoritative. Do not serialize secrets, full user profiles, or database connection details unless the threat model and retention policy explicitly support it. Redis access should use encryption, authentication, private networking, and least-privileged credentials where available.
Decide whether Redis failure should fail open, fail closed, or bypass to the source for each operation. A public article can bypass. A rate limiter may need a conservative local limit. A permission cache should not grant access because Redis is down. Implement timeouts shorter than the user request deadline and avoid retrying every cache command indefinitely. A fast failure is easier to recover from than a request stuck behind a failing cache cluster.
Observe hit rate and invalidation lag
Track hits, misses, rebuild time, stale responses, invalidation events, invalidation lag, evictions, memory use, command latency, and source fallback rate. A high hit rate can still be harmful if invalidations are delayed. Include cache key namespace and operation name in metrics, not user IDs. For a critical cache, record a source version or last-updated timestamp so an operator can compare the cached view with the authoritative record.
During incidents, log whether a response came from Redis, the source, or a stale fallback. Do not log the cached payload by default. A request ID and cache decision are usually enough to follow the path. When a value is wrong, the timeline should show source commit, invalidation publish, cache delete or refresh, and the first request that observed the new version.
Test rebuilds, races, and restarts
Test an empty cache, an expired key, concurrent misses, a failed source load, a failed cache write, an invalid serialized value, a schema version change, and a Redis restart. Test that a write cannot be hidden by an older cache refresh that finishes later. Use a controllable clock for TTL tests and a fake Redis that can inject timeouts. Verify that the application remains correct when every cache operation fails.
A Redis cache is healthy when it improves latency without changing the meaning of the system. Keep the source authoritative, define freshness and failure behavior, protect hot keys, and make invalidation observable. The cache should be something you can delete and rebuild during an incident, not a mysterious second database that only one engineer understands.
Define invalidation ownership
For each cached value, document the source of truth, key format, tenant scope, freshness promise, and the event that invalidates it. Test a write followed immediately by a read through every cache layer, including a failed invalidation and a Redis restart. Add jitter to expirations where a synchronized stampede is possible. Correctness improves when one component owns invalidation and every fallback makes staleness visible rather than silently presenting old state as authoritative.
Implementation example
For cache-aside, read the source of truth on a miss, write the value with a bounded TTL, and invalidate or version the key after a source change. Include tenant and schema identity in the key, bound value size, and decide whether a Redis failure bypasses, fails closed, or returns a stale value for each operation.
value = redis.get(key)
if value is missing: value = source.read(id); redis.set(key, value, ttl)Verify and troubleshoot
Test a write followed immediately by a read through every cache layer, a Redis restart, an invalidation failure, a schema-version change, and concurrent misses. Measure hit ratio, stale reads, stampede rate, key cardinality, command latency, and source fallback load. A cache test must prove freshness behavior, not only speed.
Operations and recovery
Jitter expirations, cap retries, and protect the source of truth from a synchronized miss storm. Keep a key-version or namespace rollback path and monitor memory, eviction, replication, and persistence state. If Redis is unhealthy, bypass safely or serve an explicitly stale response; never grant permission or entitlement because a cache value cannot be verified.
References and further reading
Use Redis command, eviction, expiration, and distributed-lock documentation, plus the cache-aside and stampede-protection patterns. State which system remains authoritative for each value.