Guide

Application caching explained: Redis, cache-aside, and invalidation

HTTP caching and CDNs speed up responses that browsers and edge nodes can reuse. Application-level caching is different: your backend stores computed results — database rows, API aggregations, session fragments — in a fast store like Redis or an in-process map so the next request skips the slow path. Done well, caching turns a database that melts under traffic into a system that feels instant. Done poorly, you serve stale prices, leak private data across users, or trigger a cache stampede that is worse than no cache at all.

Where application caching sits in the stack

Think in layers. A typical read request might hit:

  1. Browser cache — controlled by Cache-Control headers.
  2. CDN edge — caches static assets and some HTML/API responses at PoPs near users.
  3. Application cache — Redis, Memcached, or an in-memory LRU inside your API process.
  4. Database — Postgres, MySQL, or similar; often the bottleneck you are protecting.

Each layer has different granularity and invalidation rules. HTTP caches key on URLs and headers. Application caches key on arbitrary strings you design — user:48291:profile, product:sku-7721, leaderboard:daily:2026-06-07. That flexibility is powerful, but you own correctness when the underlying data changes.

Caching does not replace good schema design. Database indexes still matter; a cache is a bandage over repeated identical reads, not a fix for missing indexes or N+1 query loops.

Cache-aside (lazy loading) — the default pattern

Cache-aside is what most teams implement first. The application, not the cache library, orchestrates reads and writes:

  1. On read, check the cache for key K.
  2. Hit — deserialize the value and return it.
  3. Miss — load from the database, store in cache with a TTL, return.
  4. On write/update, update the database, then delete or overwrite key K in the cache.

Cache-aside keeps the cache optional. If Redis is down, you can fall back to the database (with higher latency) instead of failing every request. The trade-off is logic in application code: every code path that mutates data must remember to invalidate the right keys.

When cache-aside shines

Read-heavy workloads with clear keys: user profiles, product catalogs, configuration blobs, pre-rendered JSON for mobile clients. Writes are less frequent than reads, and slightly stale data for a few seconds is acceptable.

Read-through, write-through, and write-behind

Other patterns push responsibility into the cache layer or library:

Read-through

The cache itself loads missing keys from the database via a loader callback. Your app always talks to the cache API; misses are transparent. Simpler call sites, but the cache becomes a hard dependency unless you build fallback paths.

Write-through

Every write goes to the cache and the database synchronously before the client gets success. Readers always see consistent cache contents, but write latency includes cache round-trips. Use when stale reads are unacceptable and write volume is moderate.

Write-behind (write-back)

Writes land in the cache immediately; the cache flushes to the database asynchronously. Highest write throughput, highest risk: a crash before flush loses data. Reserve for analytics counters, view counts, or other eventually-persisted metrics — not financial balances.

In practice, cache-aside plus explicit invalidation covers most web APIs. Write-through appears in session stores; write-behind in high-ingest telemetry pipelines.

Redis, Memcached, and in-process caches

Redis is the default shared cache for multi-instance backends. It keeps data in memory, supports TTLs on every key, and offers rich types (strings, hashes, sorted sets) plus pub/sub for invalidation broadcasts. Persistence (RDB/AOF) is optional — treat Redis as a cache, not your system of record, unless you explicitly design for durability.

Memcached is simpler: flat key-value, multi-threaded, no persistence. Still excellent for pure caching when you do not need Redis data structures.

In-process caches (LRU maps inside each API worker) are fastest — no network hop — but each process has its own copy. Invalidation does not propagate unless you add a message bus. Good for immutable configuration loaded at startup; poor for user-specific data behind a fleet of ten pods.

For horizontally scaled deployments, consistent hashing spreads keys across Redis shards without massive reshuffling when nodes join or leave. Our consistent hashing explainer covers the ring model and virtual nodes.

Keys, TTL, and eviction

A cache key should be unique, predictable, and namespaced. Prefix by environment and entity type: prod:user:123:settings beats 123settings. Include version numbers when schema changes: v2:article:slug lets you deploy new serialization without flushing everything.

TTL (time to live) is your safety net. Even perfect invalidation misses a code path eventually; TTL guarantees stale data expires. Rules of thumb:

  • Hot public data (product listings) — 30 seconds to 5 minutes.
  • User-specific data — short TTL plus explicit delete on update.
  • Rarely changing config — longer TTL with pub/sub invalidation on admin edits.

When memory fills, Redis evicts by policy (allkeys-lru, volatile-lru, etc.). Monitor evicted keys and hit rate; rising evictions often mean undersized memory or TTLs that are too long for the working set.

Invalidation — the hard part

Phil Karlton's joke applies: there are only two hard things in computer science — cache invalidation and naming things. Strategies that work in production:

Delete on write

After updating row user 123, delete user:123:* keys. Use exact keys when possible; pattern deletes (SCAN + DEL) are expensive on large keyspaces.

Version stamps

Store a cache_version column on the parent row. Keys include the version: user:123:v7. On update, increment version — old keys become orphans that TTL out. No fan-out delete required.

Pub/sub fan-out

On write, publish invalidate:user:123. All API instances subscribe and drop local in-process entries. Pairs well with Redis as both shared cache and message bus for small teams.

Never cache what you cannot afford to leak

Keys must include tenant or user scope. Caching account-summary without a user id in the key is how one customer's balance appears to another. When responses vary by auth, vary the key — or do not cache at the edge at all.

Cache stampede (thundering herd)

A popular key expires. A thousand concurrent requests miss at once and each hammers the database with the same expensive query. Latency spikes; the database may fall over. Mitigations:

  • Probabilistic early expiration — refresh hot keys before hard TTL expiry.
  • Single-flight / request coalescing — only one worker rebuilds; others wait on the same promise.
  • Mutex per key — Redis SET key lock NX EX 5 so one process loads while others retry briefly.
  • Stale-while-revalidate — serve slightly stale data while one backend refreshes asynchronously (same idea as HTTP SWR).

Instrument cache misses per key prefix. A miss rate that jumps from 2% to 40% on one prefix is almost always a stampede or a deployment that changed key formats without warming the cache.

What to cache — and what not to

Good candidates: aggregated dashboard stats, permission lookups, geolocation tables, rendered HTML fragments, rate-limit counters (see our API rate limiting guide), and idempotent RPC responses from slow third parties.

Poor candidates: strongly consistent financial balances without careful invalidation, personally identifiable data you cannot encrypt at rest in Redis, giant blob values that exceed network MTU efficiency, and queries that are already sub-millisecond with proper indexes.

Measure before caching. If Postgres answers in 3 ms with an index, Redis adds 1 ms network RTT and operational complexity — the win may be zero until traffic multiplies.

Key takeaways

  • Application caches protect databases and downstream APIs; HTTP caches and CDNs solve different problems.
  • Cache-aside is the pragmatic default — explicit load on miss, delete on write.
  • Design namespaced keys, always set TTLs, and monitor hit rate and evictions.
  • Invalidation needs a deliberate strategy: delete-on-write, version stamps, or pub/sub — not hope.
  • Guard hot keys against cache stampedes with single-flight or early refresh.
  • Never cache cross-tenant data under a shared key; scope keys to users and environments.

Related reading