Zoom-in: Cache

It's slow, so add Redis. App gets faster, problem solved.
graph LR
C(["👤 Client"]) -->|"request"| A(["⚡ App"])
A -->|"fast response"| C
style C fill:#1e293b,stroke:#475569,color:#cbd5e1
style A fill:#1e293b,stroke:#475569,color:#cbd5e1
Zoom in on the black box.
Layer 1 — The root problem: every request is expensive
Why cache? Because some operations cost time or resources — and the result doesn't change between calls.
sequenceDiagram
participant C as Client
participant A as App
participant DB as Database
C->>A: GET /products/top-10
A->>DB: SELECT TOP 10 products ORDER BY sales
Note over DB: Scanning 10 million rows...
DB-->>A: Result (800ms)
A-->>C: List (800ms)
C->>A: GET /products/top-10 (second request)
A->>DB: SELECT TOP 10 products ORDER BY sales
Note over DB: Scanning 10 million rows again...
DB-->>A: Same result (800ms)
A-->>C: Same list (800ms)
Both requests return identical results — but each costs 800ms. The top 10 products don't change every second.
Layer 2 — Cache hit and miss: the basic mechanism
Cache follows a simple loop: check first, use it if found, compute and store if not.
graph TD
R["Request"] --> Q{"Cache has\nresult?"}
Q -->|"Cache hit ✓"| H["Return result immediately\n(few ms)"]
Q -->|"Cache miss ✗"| M["Compute result\n(800ms)"]
M --> S["Store in cache"]
S --> H2["Return result"]
style R fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style Q fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style H fill:#1a3a2a,stroke:#22c55e,color:#86efac
style H2 fill:#1a3a2a,stroke:#22c55e,color:#86efac
style M fill:#3b1a1a,stroke:#ef4444,color:#fca5a5
style S fill:#1e293b,stroke:#475569,color:#cbd5e1
Cache can't hold data forever — memory is finite and data changes. Rules are needed for how long data stays fresh and what to drop when space runs out.
Layer 3 — TTL and eviction: managing cache lifecycle
TTL (Time-To-Live) is how long an entry stays in cache before automatically expiring.
SET product:top10 <data> EX 300 # expires in 5 minutes
When the cache is full, something must be dropped — this is the eviction policy:
Short TTL → fewer stale reads, more cache misses. Long TTL → fewer DB queries, but older data. There's no "right" TTL — it depends on how much staleness the use case can tolerate.
Layer 4 — Cache invalidation: the hardest problem
Phil Karlton once said: "There are only two hard things in Computer Science: cache invalidation and naming things."
When source data changes, the cache must be updated — but the cache doesn't know that on its own.
sequenceDiagram
participant A as Admin
participant App as App
participant Cache as Redis Cache
participant DB as Database
A->>App: PUT /products/1 (update price)
App->>DB: UPDATE products SET price = 99
DB-->>App: OK
Note over Cache: Cache still holds stale data with price 149
A->>App: GET /products/top-10
App->>Cache: Fetch top-10
Cache-->>App: Stale data (price 149)
App-->>A: ❌ Wrong price
Three common strategies:
- Cache-aside + TTL: let TTL expire naturally. Simplest, accepts staleness within the TTL window.
- Write-through: update cache immediately when updating the DB. More consistent, more complex.
- Event-driven invalidation: when DB changes, emit an event to delete the related cache entry. Most flexible, but requires additional infrastructure.
Full picture
A request traversing cache layers before reaching the database.
graph LR
C["👤 Client"] --> BC["Browser Cache\n(Service Worker / HTTP cache)"]
BC -->|"miss"| CDN["🌐 CDN\n(CloudFront, Cloudflare)"]
CDN -->|"miss"| AC["⚡ App Cache\n(Redis, Memcached)"]
AC -->|"miss"| DB["🗄️ Database"]
DB -->|"result"| AC
AC -->|"store + return"| CDN
CDN -->|"store + return"| BC
BC -->|"store + return"| C
style C fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style BC fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style CDN fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style AC fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style DB fill:#1a3a2a,stroke:#22c55e,color:#86efac
Each layer solves a different problem: browser cache eliminates network round trips; CDN reduces geographic latency; app cache reduces DB load. When debugging, the key question is which layer is serving the cached response.
Three common misconceptions
Redis is a database
Redis is an in-memory store — data is lost on restart unless persistence is enabled. Use Redis for cache (loss is acceptable) or sessions (with an explicit TTL), not for critical data that needs to survive long-term.
More cache is always better
Cache keys need management: too many entries leads to unpredictable eviction; caching frequently-changing data leads to constant staleness. Cache works best for data that's read often, changes rarely, and can tolerate being a few seconds to minutes behind.
Deleting the cache is enough to update it
Cache deletion (invalidation) isn't atomic with the DB update — the window between deleting the cache and committing to the DB can let another request read stale data and write it back into the cache. In concurrent environments, operation order matters more than most people assume.
Takeaway
Cache exists because some operations cost more than the value of perfect consistency. TTL is a commitment to the acceptable level of staleness; eviction policy is the strategy when memory runs out; cache invalidation is hard because the DB and cache are two independent systems with no awareness of each other.
When debugging wrong data, the first question is: "which cache layer is this request reading from?" — browser devtools, CDN headers (X-Cache: HIT), and Redis TTL <key> will point to the problematic layer.
Article assisted by Amy 🌸 - AI Assistant. Content reviewed by the author.
Related Posts
Zoom-in: TCP
Every HTTP request runs on TCP — but before the first byte of real data crosses the wire, three packets are exchanged carrying no data at all. TCP solves the problem the Internet doesn't.
Zoom-in: OAuth 2.0
'Sign in with Google' hides a delegation mechanism where your password never leaves Google. OAuth 2.0 solves the authorization problem without sacrificing security.
Zoom-in: Load Balancer
One domain, millions of requests per day. A load balancer doesn't just split traffic — it decides routing, health checking, and session management for the entire system.