vM.

How to Add Redis Caching to an API Without Serving Stale Data Forever

Author
Vishal Maurya
Published on
Reading time
4 min read

Overview

Redis can reduce repeated database work and improve response times for data that is requested frequently. The difficult part is not writing a value to Redis; it is deciding when that value is safe to reuse and how it should be refreshed after the source changes.

A cache should be an optimization, not the only place where important business data exists.

1. Choose a Cacheable Result

Caching is useful when a result is expensive to compute, requested repeatedly, and allowed to be slightly stale. It is less useful for rapidly changing data or responses that depend on many user-specific permissions.

Start with one measured endpoint. Record its query time, request frequency, and freshness requirement. Do not cache a slow response until you understand whether the real issue is an N+1 query, a missing index, or excessive response size.

2. Build Cache Keys from All Relevant Inputs

A key must represent every input that changes the result. If the response varies by tenant, user permissions, locale, filter, or API version, omitting that dimension can return the wrong data.

import hashlib
import json


def make_cache_key(*, tenant_id: str, filters: dict) -> str:
    normalized = json.dumps(
        filters,
        sort_keys=True,
        separators=(",", ":"),
    )
    digest = hashlib.sha256(normalized.encode()).hexdigest()
    return f"invoice-list:v1:tenant:{tenant_id}:{digest}"

This example builds a deterministic key for filters and a tenant. Add any other dimension that affects the response. Do not include secrets in cache keys, and be aware that hashing a key does not solve authorization by itself.

3. Set a TTL Based on the Freshness Requirement

A time-to-live limits how long an entry remains available without explicit invalidation. It does not guarantee that the data stays fresh during that interval.

For a dashboard summary, a short TTL may be acceptable. For access permissions, payment state, or sensitive account status, stale values may be unsafe. Those cases may require a different approach rather than simply choosing a smaller TTL.

Document the acceptable staleness for each cached result and choose the TTL accordingly.

4. Decide How Updates Invalidate the Cache

Common strategies include:

  • TTL-only: simple, but changes remain invisible until expiry.
  • Explicit deletion: remove affected keys when the source changes; difficult when many key variants exist.
  • Versioned keys: include a version or generation number that changes after updates.
  • Event-driven invalidation: publish a change event, with care for delivery failures and ordering.

For list endpoints with many filter combinations, deleting every possible key can be impractical. A versioned namespace or a cache design with bounded key families may be easier to operate.

If the database update succeeds but cache invalidation fails, the application needs a defined fallback. A transactional outbox can help coordinate durable change events when eventual invalidation is acceptable.

5. Prevent Cache Stampedes

When a popular key expires, many concurrent requests can miss at once and run the same expensive query. Depending on the workload, use a short lock, request coalescing, stale-while-revalidate behavior, or randomized TTLs to spread refreshes.

Locking requires care: set an expiry, handle lock-owner failure, and avoid allowing an old worker to overwrite a newer result. Not every endpoint needs this complexity; measure whether synchronized misses are actually a problem.

6. Decide What Happens When Redis Is Unavailable

For a non-critical cache, the application may fall back to the database. That fallback can create a sudden load spike if Redis is down, so use timeouts and consider concurrency limits or circuit-breaking behavior.

Do not make the database fallback unlimited. A cache outage should not turn into a database outage. For queues or other Redis-backed durable workflows, the failure policy is different from an ordinary response cache; do not treat them as interchangeable use cases.

7. Measure Hit Rate and Correctness

Track hit rate, miss rate, cache latency, evictions, key count, memory usage, and database load. A high hit rate does not prove correctness. Test that an update becomes visible within the documented freshness window and that one tenant or permission context cannot receive another's cached response.

Conclusion

Good caching requires a clear freshness contract, complete cache keys, deliberate invalidation, and a safe failure mode. Start with one endpoint, measure the result, and expand only when the benefit outweighs the consistency and operational cost.

If your API needs Redis caching or has stale-data and cache-invalidation problems, I can help design the key strategy and update workflow. Contact me.

Additional Resources

  • Redis documentation
  • Django cache framework