---
# === IDENTITY ===
id: software/patterns/caching-patterns/2026
canonical_question: "What are the caching patterns (cache-aside, write-through, write-behind)?"
aliases:
  - "Which caching strategy should I use?"
  - "Cache-aside vs read-through vs write-through comparison"
  - "How to implement write-behind caching"
  - "Redis caching patterns best practices"
entity_type: software_reference
domain: software > patterns > caching patterns
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.92
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: null
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Never cache without a TTL -- unbounded caches cause memory exhaustion in production"
  - "Write-behind risks data loss if the cache node crashes before async flush completes"
  - "Cache invalidation must be atomic with the database write or bounded by a short TTL"
  - "Distributed caches require consistent hashing or a coordination layer for partition tolerance"
  - "Never cache authentication tokens or secrets without encryption at rest"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Looking for HTTP/browser caching (Cache-Control, ETag, CDN)"
    use_instead: "software/system-design/cdn-design/2026"
  - condition: "Looking for multi-layer caching architecture design"
    use_instead: "software/system-design/multi-layer-caching/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "workload_type"
    question: "Is the workload read-heavy, write-heavy, or balanced?"
    type: choice
    options: ["read-heavy", "write-heavy", "balanced"]
  - key: "consistency_requirement"
    question: "How important is strong consistency vs performance?"
    type: choice
    options: ["strong-consistency", "eventual-consistency", "best-effort"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/caching-patterns/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/patterns/sql-materialized-views/2026"
      label: "SQL Materialized Views"
    - id: "software/debugging/postgresql-connection-pool/2026"
      label: "Connection Pooling"
  solves:
    - id: "software/debugging/redis-memory-issues/2026"
      label: "Redis Memory Issues"
  alternative_to: []
  often_confused_with:
    - id: "software/system-design/cdn-design/2026"
      label: "CDN Design (HTTP/browser caching)"
    - id: "software/system-design/multi-layer-caching/2026"
      label: "Multi-Layer Caching Architecture"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "Database Caching Strategies Using Redis"
    author: AWS
    url: https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/caching-patterns.html
    type: official_docs
    published: 2024-06-15
    reliability: high
  - id: src2
    title: "Cache-Aside Pattern"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside
    type: official_docs
    published: 2024-09-20
    reliability: high
  - id: src3
    title: "Read-Through, Write-Through, Write-Behind Caching and Refresh-Ahead"
    author: Oracle
    url: https://docs.oracle.com/cd/E13924_01/coh.340/e13819/readthrough.htm
    type: official_docs
    published: 2023-01-01
    reliability: high
  - id: src4
    title: "Why Your Caching Strategies Might Be Holding You Back"
    author: Redis
    url: https://redis.io/blog/why-your-caching-strategies-might-be-holding-you-back-and-what-to-consider-next/
    type: technical_blog
    published: 2025-08-12
    reliability: moderate_high
  - id: src5
    title: "Caching Challenges and Strategies"
    author: AWS
    url: https://aws.amazon.com/builders-library/caching-challenges-and-strategies/
    type: technical_blog
    published: 2024-03-01
    reliability: high
  - id: src6
    title: "A Hitchhiker's Guide to Caching Patterns"
    author: Hazelcast
    url: https://hazelcast.com/blog/a-hitchhikers-guide-to-caching-patterns/
    type: technical_blog
    published: 2024-05-10
    reliability: moderate_high
  - id: src7
    title: "Cache Stampede - Wikipedia"
    author: Wikipedia
    url: https://en.wikipedia.org/wiki/Cache_stampede
    type: community_resource
    published: 2025-01-15
    reliability: moderate_high
---

# Caching Patterns: Cache-Aside, Write-Through, Write-Behind, and Beyond

## TL;DR

- **Bottom line**: Use cache-aside (lazy loading) for most read-heavy workloads; it is the simplest, most battle-tested pattern with the fewest surprises.
- **Key tool/command**: `SET key value EX ttl_seconds` (Redis) with TTL on every key -- never cache without expiration.
- **Watch out for**: Cache invalidation is the hardest problem in computer science -- stale data, cache stampedes, and cold-start thundering herds cause more production incidents than cache misses.
- **Works with**: Redis 7.x, Memcached 1.6.x, Hazelcast 5.x, any language with a Redis client. Patterns are technology-agnostic.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never cache without a TTL -- unbounded caches cause memory exhaustion in production
- Write-behind risks data loss if the cache node crashes before async flush completes -- use only when data loss is tolerable or the cache is replicated
- Cache invalidation must be atomic with the database write or bounded by a short TTL to prevent serving stale data beyond the consistency window
- Distributed caches require consistent hashing or a coordination layer; naive sharding leads to hot spots and split-brain scenarios
- Never cache authentication tokens, API keys, or secrets without encryption at rest and strict access controls

## Quick Reference

| Pattern | Read Path | Write Path | Consistency | Complexity | Best For |
|---|---|---|---|---|---|
| **Cache-Aside** (Lazy Loading) | App checks cache; on miss, reads DB, writes to cache | App writes to DB; invalidates or ignores cache | Eventual (TTL-bounded) | Low | Read-heavy workloads, general purpose |
| **Read-Through** | Cache itself fetches from DB on miss | App writes to DB; cache auto-populated on next read | Eventual (TTL-bounded) | Medium | Read-heavy with cache-provider support |
| **Write-Through** | App reads from cache (always populated) | App writes to cache; cache synchronously writes to DB | Strong | Medium | Read-heavy where consistency matters |
| **Write-Behind** (Write-Back) | App reads from cache (always populated) | App writes to cache; cache asynchronously flushes to DB | Eventual (delay-bounded) | High | Write-heavy workloads, batch writes |
| **Refresh-Ahead** | Cache proactively refreshes before TTL expires | Same as underlying pattern | Strong (if refresh succeeds) | High | Hot keys with predictable access |
| **Distributed Cache** | Hash-routed to correct shard; local read | Hash-routed to correct shard; replicated writes | Eventual or strong (configurable) | High | Large-scale, multi-node deployments |

## Decision Tree

```
START
|-- Read-heavy workload (>80% reads)?
|   |-- YES --> Need strong consistency?
|   |   |-- YES --> Write-through (cache always in sync with DB)
|   |   +-- NO --> Cache-aside (simplest, most flexible)
|   +-- NO |
|-- Write-heavy workload (>50% writes)?
|   |-- YES --> Can tolerate small data loss window?
|   |   |-- YES --> Write-behind (best write throughput)
|   |   +-- NO --> Write-through (safe but slower writes)
|   +-- NO |
|-- Balanced read/write?
|   |-- YES --> Need automatic cache population?
|   |   |-- YES --> Read-through + write-through combo
|   |   +-- NO --> Cache-aside (manual control)
|   +-- NO |
|-- Hot keys with predictable access?
|   |-- YES --> Refresh-ahead (pre-warm before expiry)
|   +-- NO |
+-- DEFAULT --> Cache-aside with TTL (safest starting point)
```

## Step-by-Step Guide

### 1. Choose your caching layer

Select Redis (most common), Memcached (simpler, multi-threaded), or an in-process cache (e.g., Caffeine for JVM, node-cache for Node.js). Redis is the default choice for most applications due to its data structure support and persistence options. [src1]

```bash
# Install Redis (Docker)
docker run -d --name redis -p 6379:6379 redis:7-alpine

# Verify connection
redis-cli ping
# Expected: PONG
```

**Verify**: `redis-cli ping` --> expected output: `PONG`

### 2. Implement cache-aside pattern

The application is responsible for reading from and writing to the cache. On a cache miss, read from the database, then populate the cache. On a write, update the database and invalidate the cache entry. [src2]

```javascript
// Node.js + ioredis cache-aside
const Redis = require('ioredis');
const redis = new Redis();

async function getUser(userId) {
  const cacheKey = `user:${userId}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);          // cache hit

  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // TTL 1h
  return user;
}
```

**Verify**: Check cache hit ratio after deployment: `redis-cli INFO stats | grep keyspace_hits`

### 3. Add cache invalidation on writes

When data changes, either delete the cache key (invalidation) or update it (refresh). Deletion is safer -- it avoids race conditions between concurrent reads and writes. [src5]

```javascript
async function updateUser(userId, data) {
  await db.query('UPDATE users SET name = $1 WHERE id = $2', [data.name, userId]);
  await redis.del(`user:${userId}`); // invalidate, don't update
}
```

**Verify**: `redis-cli EXISTS user:123` --> expected: `(integer) 0` after update

### 4. Add TTL jitter to prevent stampedes

If many keys expire at the same time, all requests hit the database simultaneously (thundering herd). Add random jitter to spread expirations. [src7]

```javascript
// Add 10% jitter to TTL
const baseTTL = 3600; // 1 hour
const jitter = Math.floor(Math.random() * baseTTL * 0.1);
await redis.set(key, value, 'EX', baseTTL + jitter);
```

**Verify**: `redis-cli TTL user:123` --> should vary between 3600-3960

### 5. Implement cache stampede protection

Use a distributed lock to ensure only one process recomputes a cache miss while others wait or serve stale data. [src7]

```javascript
async function getWithLock(key, computeFn, ttl = 3600) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:${key}`;
  const acquired = await redis.set(lockKey, '1', 'EX', 30, 'NX');
  if (acquired) {
    try {
      const value = await computeFn();
      await redis.set(key, JSON.stringify(value), 'EX', ttl);
      return value;
    } finally {
      await redis.del(lockKey);
    }
  }
  // Wait and retry if lock not acquired
  await new Promise(r => setTimeout(r, 100));
  return getWithLock(key, computeFn, ttl);
}
```

**Verify**: Under load test, database queries for the same key should be 1, not N.

## Code Examples

### Node.js + Redis: Cache-Aside with Write-Through

> Full script: [node-js-redis-cache-aside-with-write-through.js](scripts/node-js-redis-cache-aside-with-write-through.js) (25 lines)

```javascript
// Input:  Redis connection, database connection
// Output: Cached read with consistent write-through
const Redis = require('ioredis');       // ioredis@5.x
const redis = new Redis({ host: '127.0.0.1', port: 6379 });
// Cache-aside read
# ... (see full script)
```

### Python + Redis: Cache-Aside with Decorator

```python
# Input:  Redis connection, decorated function
# Output: Transparent caching via decorator

import json, random, functools
import redis  # redis-py>=5.0

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

def cached(ttl=3600, prefix="cache"):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            key = f"{prefix}:{fn.__name__}:{args}:{kwargs}"
            hit = r.get(key)
            if hit:
                return json.loads(hit)
            result = fn(*args, **kwargs)
            jitter = random.randint(0, int(ttl * 0.1))
            r.set(key, json.dumps(result), ex=ttl + jitter)
            return result
        return wrapper
    return decorator

@cached(ttl=1800)
def get_product(product_id: int) -> dict:
    """Fetches product from DB; cached for 30 min."""
    return db.execute("SELECT * FROM products WHERE id = %s", (product_id,))
```

### Go: Cache-Aside with singleflight (Stampede Protection)

> Full script: [go-cache-aside-with-singleflight-stampede-protecti.go](scripts/go-cache-aside-with-singleflight-stampede-protecti.go) (36 lines)

```go
// Input:  Redis client, database connection
// Output: Cache-aside reads with built-in stampede protection
package main
import (
    "context"
# ... (see full script)
```

## Anti-Patterns

### Wrong: Caching without TTL

```javascript
// BAD -- no TTL means cache entries live forever
// Memory grows unbounded; stale data served indefinitely
await redis.set(`user:${id}`, JSON.stringify(user));
```

### Correct: Always set a TTL

```javascript
// GOOD -- bounded TTL with jitter
const ttl = 3600 + Math.floor(Math.random() * 360);
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', ttl);
```

### Wrong: Update cache instead of invalidating on write

```javascript
// BAD -- race condition between concurrent read and write
async function updateUser(id, data) {
  await db.update(id, data);
  const updated = await db.get(id);     // another write may happen here
  await redis.set(`user:${id}`, JSON.stringify(updated), 'EX', 3600);
}
```

### Correct: Delete cache key on write (invalidation)

```javascript
// GOOD -- delete is idempotent and race-free
async function updateUser(id, data) {
  await db.update(id, data);
  await redis.del(`user:${id}`);        // next read will re-populate
}
```

### Wrong: No stampede protection on cache miss

```javascript
// BAD -- 1000 concurrent requests all hit DB on cache miss
async function getProduct(id) {
  const cached = await redis.get(`product:${id}`);
  if (cached) return JSON.parse(cached);
  const product = await db.query('SELECT * FROM products WHERE id = $1', [id]);
  await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 3600);
  return product;
}
```

### Correct: Use locking or singleflight to coalesce requests

```javascript
// GOOD -- only one request recomputes; others wait
async function getProduct(id) {
  const cached = await redis.get(`product:${id}`);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:product:${id}`;
  const locked = await redis.set(lockKey, '1', 'EX', 10, 'NX');
  if (!locked) {
    await sleep(50);
    return getProduct(id);              // retry after brief wait
  }
  try {
    const product = await db.query('SELECT * FROM products WHERE id = $1', [id]);
    await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 3600);
    return product;
  } finally {
    await redis.del(lockKey);
  }
}
```

## Common Pitfalls

- **Cache stampede (thundering herd)**: When a popular key expires, hundreds of concurrent requests hit the database simultaneously. Fix: use distributed locking (`SET key NX EX`), singleflight (Go), or probabilistic early expiration. [src7]
- **Cold start after deployment**: A fresh cache means 100% miss rate, causing a burst of database queries. Fix: implement cache warming on startup -- preload the top N most-accessed keys from the database. [src5]
- **Serialization overhead**: JSON.stringify/parse on every cache hit adds CPU cost, especially for large objects. Fix: use MessagePack, Protocol Buffers, or store pre-serialized responses. Profile before optimizing. [src4]
- **Cache-database inconsistency window**: Between a DB write and cache invalidation, stale data may be served. Fix: set short TTLs (5-60s for volatile data) and invalidate immediately after the DB write. [src1]
- **Hot key problem**: A single key receiving disproportionate traffic can overwhelm one Redis shard. Fix: replicate hot keys across shards with key suffixes (`product:123:shard1`, `product:123:shard2`) or use local in-process caching for the hottest keys. [src6]
- **Over-caching**: Caching every database query, including rarely-accessed data, wastes memory and adds invalidation complexity. Fix: cache only data with a read:write ratio > 10:1 and measure hit rates. [src5]

## Diagnostic Commands

```bash
# Check Redis memory usage
redis-cli INFO memory | grep used_memory_human

# Check cache hit ratio (higher is better; aim for >90%)
redis-cli INFO stats | grep keyspace
# keyspace_hits / (keyspace_hits + keyspace_misses) = hit ratio

# Monitor slow commands (>10ms)
redis-cli SLOWLOG GET 10

# Check TTL on a specific key
redis-cli TTL user:123

# Monitor real-time commands (use sparingly in production)
redis-cli MONITOR | head -50

# Check connected clients
redis-cli INFO clients | grep connected_clients

# Check eviction policy
redis-cli CONFIG GET maxmemory-policy
```

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Read:write ratio exceeds 10:1 | Data changes on every request | Direct database queries with connection pooling |
| Database query latency > 50ms and same query repeats often | Strong consistency is non-negotiable and TTL window is unacceptable | Synchronous read-through with write-through |
| Need to absorb traffic spikes without scaling the database | Working with small datasets that fit in application memory | In-process caching (Caffeine, node-cache, lru-cache) |
| Multiple application instances need shared cache state | Caching would only save <5ms per request | No cache -- the overhead is not worth the complexity |
| Session storage, API rate limiting, leaderboards | Data has complex relational joins that change frequently | Materialized views in the database |

## Important Caveats

- Cache-aside is not a silver bullet -- it adds a network round-trip on every read (cache check), which can be slower than a well-indexed database query for simple lookups
- Write-behind pattern requires careful failure handling: if the cache crashes before flushing to the database, data is lost. Use Redis AOF persistence or replicated setups to mitigate this risk
- Memcached does not support persistence, replication, or data structures beyond key-value strings. Use Redis if you need sorted sets, streams, or crash recovery
- Redis single-threaded model means a single slow command (e.g., `KEYS *`) blocks all other operations. Use `SCAN` for iteration in production
- Cache invalidation in microservices architectures often requires event-driven patterns (pub/sub, CDC) rather than direct delete calls, because the service that writes may not know all consumers

## Related Units
<!-- Generated from related_kos frontmatter -->

- [SQL Materialized Views](/software/patterns/sql-materialized-views/2026)
- [Connection Pooling](/software/debugging/postgresql-connection-pool/2026)
- [Redis Memory Issues](/software/debugging/redis-memory-issues/2026)
- [CDN Design](/software/system-design/cdn-design/2026)
- [Multi-Layer Caching Architecture](/software/system-design/multi-layer-caching/2026)
