How to serve data at lightning speed | Caching Strategies (Redis/Memcached)
The Performance Problem: Database Bottlenecks
Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached.
🎯 System Scenario & Requirements
System Goal
Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached.
Actors & Components
- Application Workers
- Volatile Memory Tier
- Persistent Disk Tier
- Worker Process
- In-Memory Cache
- Primary Database
Functional Requirements
- TLDR: Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached.
- Selecting the correct caching strategy—such as Cache-Aside, Write-Through, or Write-Behind—determines how data consistency is maintained between the cache and the primary database.
- In modern web development, database access is often the primary bottleneck.
Constraints & Assumptions
- Constraints are derived from the rebuilt scenario stages.
⚡ Execution Stages & Decision Trace
Stage 1 — Caching Patterns
The flow of data between the application, the cache, and the database is governed by specific caching patterns.
Cache-Aside (Lazy Loading) This is the most common caching pattern.
Deep Dive
Scenario context
Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached. Selecting the correct caching strategy—such as Cache-Aside, Write-Through, or Write-Behind—determines how data consistency is maintained between the cache and the primary database.
Observable outcome: - Read Flow: The application checks the cache.
Stage 2 — Eviction Policies and TTL
- Memory (RAM) is expensive and limited.
- A cache cannot grow indefinitely.
- To manage memory constraints, caches use two mechanisms: Time-To-Live (TTL) and Eviction Policies .
- Time-To-Live (TTL) Every cached key should be assigned an expiration timer (TTL).
Observable outcome: Time-To-Live (TTL) Every cached key should be assigned an expiration timer (TTL).
Stage 3 — Caching Gotchas and Mitigation
- Cache Avalanche - The Problem: A large number of cached keys expire at the exact same time, or the cache server crashes.
- This causes all client traffic to hit the database simultaneously, causing a database crash.
- Mitigation: Add a random jitter (e.g., adding a random offset of 1 to 5 minutes) to the TTL of every key, spreading expiration events evenly over time.
- Cache Penetration - The Problem: Clients request keys that do not exist in either the cache or the database (e.g., searching for a non-existent user ID).
Observable outcome: Cache Penetration - The Problem: Clients request keys that do not exist in either the cache or the database (e.g., searching for a non-existent user ID).
Stage 4 — Write-Around: Keep One-Time Writes Out of the Cache
- Write-around sends writes directly to the database and invalidates any old cached value.
- The cache is populated only if a later read needs the record.
- It is useful for imports, audit events, large objects, and other data that is written once but may never become hot.
- Its cost is predictable: the first read after every write is a cache miss.
Observable outcome: Its cost is predictable: the first read after every write is a cache miss.
Stage 5 — Practical Cache-Aside with Stampede Protection
- ~~~ts async function getProduct(id: string) { const key = 'product:v3:' + id; const cached = await redis.get(key); if (cached) return JSON.parse(cached); // SET NX lets one request rebuild the key.
- Others briefly retry or serve stale data.
- const lockKey = key + ':lock'; const ownsLock = await redis.set(lockKey, requestId, { NX: true, PX: 3 000 }); if (!ownsLock) return retryWithJitter(() = getProduct(id), 40, 120); try { const product = await db.products.…
- ttlSeconds : 30 }); return product; } finally { // Production code should compare the lock token before deletion.
Observable outcome: ttlSeconds : 30 }); return product; } finally { // Production code should compare the lock token before deletion.
Stage 6 — Capacity Planning and Observability
- Suppose an API serves 20,000 reads per second, the database safely handles 2,500, and the target cache hit ratio is 90%.
- The expected database read rate is 2,000 per second: within budget, but with little room for a stampede.
- At 80% hits the database receives 4,000 reads per second and is already overloaded.
- Capacity planning must test degraded hit ratios, not only the happy path.
Observable outcome: Capacity planning must test degraded hit ratios, not only the happy path.
End-to-end outcome
After working through the stages, you should be able to explain:
- A cache reduces load only while its miss path remains controlled.
- Pattern choice is primarily a consistency and failure-semantics decision.
Alternatives considered
Decision Guide: Selecting the Caching Pattern
Use this guide to choose the caching strategy for your system: - Use Cache-Aside if: - Your application is read-heavy and you want to cache data dynamically.
Redis vs Memcached: Choose Capabilities, Not Brand Recognition
Choose Memcached when values are disposable blobs, operations are simple, and rebuilding the entire cache is acceptable.
Failure and degraded modes
The source does not isolate a separate failure mode. Limits that matter for the scenario are called out in the stage decisions above.
