Call
Redis

Redis Database Query Caching: Cache-Aside, Invalidation, and Stampede Control

Build a correct Redis query cache with cache-aside, deterministic keys, TTL-based staleness, write invalidation, stampede protection, negative caching, and safe failure behavior.

Published: May 1, 2026
Updated: September 12, 2026
9 min read
Redis Database Query Caching: Cache-Aside, Invalidation, and Stampede Control

Redis Database Query Caching: Cache-Aside, Invalidation, and Stampede Control

Database query caching helps when the same expensive reads repeat far more often than the underlying data changes. Redis is a natural fit because the application can check an in-memory cache first, query the primary database only on a miss, then reuse that result for later requests.

The hard part is not GET and SET. The hard part is cache correctness: deciding what deserves caching, designing stable keys, bounding staleness, invalidating after writes, and preventing a popular expired key from sending a traffic spike straight back to the database.

Start with cache-aside

For most application-level query caching, cache-aside is the easiest pattern to reason about.

The read path is:

  1. Build a deterministic cache key.
  2. Read Redis.
  3. On a hit, return the cached value.
  4. On a miss, query the primary database.
  5. Store the result in Redis with a TTL.
  6. Return the database result.

Conceptually:

key = cache:product:42

value = redis GET key
if value exists:
    return value

value = database query
redis SET key value EX 300
return value

Redis' current cache-aside guidance recommends the same basic model and treats the primary database as the source of truth.

Cache results, not arbitrary SQL strings

A fragile implementation hashes the raw SQL text and uses that as the cache identity. Small changes in whitespace, ORM output, selected columns, parameter ordering, or generated aliases can produce different keys for logically identical reads.

Prefer keys based on the meaning of the query:

cache:product:v3:42
cache:category-page:v2:shoes:page:1
cache:tenant:t42:dashboard-summary:v5

A good cache key includes the inputs that actually change the result:

  • entity ID or normalized filter parameters
  • tenant/account scope
  • locale or currency when relevant
  • permission scope when the response differs by authorization
  • a schema/version component when cached representation changes

Do not put passwords, access tokens, personal secrets, or raw sensitive search input into Redis key names.

Cache only reads with a useful reuse window

A query is a good cache candidate when:

  • it is requested repeatedly
  • the result changes less often than it is read
  • the query is meaningfully expensive or contributes to database load
  • serving a slightly stale result is acceptable, or invalidation can keep it fresh enough

Poor candidates include:

  • highly personalized one-off queries
  • rapidly changing values where stale reads are unacceptable
  • cheap indexed lookups with little repetition
  • transactional reads that must reflect the immediately committed state
  • huge result sets that consume more Redis memory than they save in database work

Caching should remove real pressure from a measured bottleneck, not become a default wrapper around every repository method.

TTL is a staleness budget

A TTL is not merely cleanup. It defines how long cached data may survive without explicit invalidation.

For example:

SET cache:product:42 encoded-product EX 300

A five-minute TTL means an entry can remain cached for up to that window unless the write path removes it sooner.

Shorter TTLs provide fresher data but create more misses. Longer TTLs improve hit rate but increase the stale-data window.

Choose TTLs by business semantics:

DataTypical reasoning
product descriptioncan often tolerate a longer TTL
inventory availabilityneeds much tighter freshness
tenant configurationmay use explicit invalidation plus a fallback TTL
public reference datacan often be cached aggressively
security/permission decisionusually deserves stricter freshness and careful invalidation

There is no universal "best" cache TTL.

Invalidate after the primary write succeeds

In cache-aside, a practical write flow is:

  1. Update the primary database.
  2. Commit successfully.
  3. Delete the affected Redis key.
  4. Let the next read repopulate it from the primary.

Example:

UPDATE product in primary database
DEL cache:product:42

Redis' cache-aside guidance favors invalidating the cached copy rather than trying to maintain a second perfectly synchronized copy in the cache.

Why delete instead of rewrite? The database remains the source of truth, and the next read reconstructs the cache using the same normal read path. That reduces duplicated cache-population logic.

Complex queries need an invalidation strategy before they are cached

Caching a record by ID is straightforward because a write to product 42 clearly invalidates cache:product:42.

Caching aggregate queries is harder:

cache:top-products:electronics
cache:tenant:t42:monthly-summary
cache:search:brand:nova:page:1

One database write may affect many cached results.

Possible strategies include:

  • short TTLs for aggregates where small staleness is acceptable
  • explicit dependency tracking for a small, known set of derived keys
  • namespace versioning, such as incrementing a category cache version
  • event-driven invalidation when writes emit reliable domain events
  • avoiding the cache entirely when invalidation cost exceeds the benefit

The last option is underrated. A cache that engineers cannot invalidate correctly is often worse than a slower query.

Protect against cache stampedes

A cache stampede happens when a hot key expires and many concurrent requests miss at the same time. Every request then queries the primary database for the same value.

This can turn one expired key into a sudden database spike.

Common mitigations include:

Single-flight or short lock

Allow one request to rebuild the value while concurrent callers wait briefly or reuse an older value when your design supports it.

Redis' current cache-aside examples use a small atomic coordination mechanism for this purpose.

TTL jitter

If thousands of keys are created together with exactly the same TTL, they can expire together.

Instead of giving every key precisely 300 seconds, add a small randomized component:

base TTL = 300 seconds
jitter = small random addition

The goal is not random caching behavior. It is to spread expiration work across time.

Early refresh

For extremely hot values, refresh them shortly before expiry rather than waiting for a hard miss.

Use this selectively. Most keys do not need elaborate refresh machinery.

Consider negative caching

A database miss can be expensive too.

If clients repeatedly request a non-existent entity, caching that "not found" result for a short period can protect the primary database:

cache:product:999 = NOT_FOUND
TTL = short

Keep negative TTLs conservative because the missing record may be created shortly afterward.

Also distinguish a genuine "not found" result from a database timeout or dependency failure. Do not turn transient failures into cached business truth.

Prevent cross-tenant and authorization leaks

A cache hit bypasses the database query that might otherwise enforce tenant filtering. That makes cache-key scoping part of the authorization boundary.

Bad key:

cache:dashboard:user:42

Safer when the result is tenant-specific:

cache:tenant:t7:dashboard:user:42

If the same resource is rendered differently by role, locale, plan, currency, feature flags, or permissions, those dimensions may need to participate in the key or the cache must occur below that transformation layer.

Never reuse a cached response across security contexts merely because the underlying URL looks identical.

Decide what happens when Redis is unavailable

For ordinary cache-aside, Redis should usually be an optimization rather than the only path to the data.

A common degraded mode is:

  1. Redis read fails.
  2. Query the primary database directly.
  3. Return the result.
  4. Avoid turning every request into repeated slow Redis retries.

But the fallback itself can overload the primary during a widespread cache outage. Production systems often need:

  • bounded Redis timeouts
  • circuit-breaking or backoff
  • database concurrency limits
  • load shedding for expensive endpoints
  • enough primary capacity for a realistic degraded mode

A cache outage should not automatically become a database outage.

Strings, Hashes, or JSON?

For a cached result that is always read as a whole, a serialized String is usually enough:

SET cache:product:42 encoded-object EX 300

Redis Hashes make sense when fields are accessed or changed independently. Redis JSON can make sense for nested document access and indexing.

Do not choose a more complex type without a corresponding access pattern.

Avoid caching giant unbounded query results

Caching a query that returns tens of thousands of rows can create several problems:

  • large network transfers
  • expensive serialization
  • large allocations in the application
  • memory pressure in Redis
  • longer invalidation/repopulation time

Cache smaller entities, bounded pages, summaries, or precomputed results instead.

If a report is intrinsically heavy, consider materialization, analytics storage, read replicas, or query/index optimization rather than forcing it through Redis.

Measure whether the cache is helping

At minimum, track:

  • hit rate
  • miss rate
  • cache read latency
  • database latency after cache misses
  • primary query volume before/after caching
  • key memory usage
  • evictions
  • stampede/single-flight waits
  • cache errors and fallback rate

A high hit rate is not enough by itself. You can have a 95% hit rate on cheap queries while the remaining 5% still contains the real database bottleneck.

Measure the database work avoided, not just the number of Redis hits.

Common query-caching mistakes

Caching before optimizing the SQL

A missing index or bad query plan does not become good architecture because Redis hides it most of the time.

No invalidation plan

If engineers cannot explain exactly when a cached value becomes stale and how it is replaced, the design is incomplete.

One TTL for every type of data

Different data has different freshness requirements.

Raw SQL as the cache identity

ORM/query-shape noise creates duplicate cache entries and brittle behavior.

Ignoring cache stampedes

A popular key's expiry can create more database load than the steady-state cache saved.

Ignoring tenant or permission context

A fast cache hit that returns another tenant's data is a severe security bug.

Treating every query as cacheable

Redis memory is finite. Cache the hot, reusable working set rather than duplicating the entire database by accident.

A practical baseline

For a read-heavy backend, a strong starting design is:

  • use cache-aside
  • build semantic, deterministic keys
  • include tenant/security dimensions that affect the result
  • give every cached entry an explicit TTL
  • update the database first, then invalidate the cache
  • use short negative caching only for genuine misses
  • protect hot keys from stampedes
  • add TTL jitter when synchronized expiry is plausible
  • make Redis failure degrade deliberately
  • monitor both cache metrics and database work avoided

Database query caching pays off when it has a clear correctness contract: what is cached, how stale it may become, what invalidates it, and what happens on a miss or cache failure.

Official references

Tags:RedisDatabase CachingCache-AsidePerformanceScalabilityBackend Architecture
ENTERPRISE SAAS ENGINEERING

BUILDING A SAAS PRODUCT?

Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.

EXPLORE SAAS SERVICES →