system-design · intermediate

Caching 101 — Memory Offloading and Latency Reduction

The Central Question

Consider an e-commerce platform running on the CacheLab platform (cachelab.com) processing 50,000 requests per second:


If every incoming request forces the database to re-read disk blocks and re-compute identical SQL join operations, the database will exhaust CPU and crash under load.

A cache is a temporary high-speed data storage layer (typically operating in volatile RAM) that stores previously computed results so subsequent requests can serve the payload in sub-milliseconds without querying the primary source of truth.

This lesson answers one central question: How do engineers model Cache Hit Ratios (H), calculate Average Memory Access Time (AMAT), implement Cache-Aside access patterns, and manage TTL expiration to protect database infrastructure while maintaining data freshness?


The Hardware Memory Pyramid: RAM vs. Disk Physics

Caching takes advantage of computer hardware physics: RAM is orders of magnitude faster than persistent SSD storage, but carries a higher cost per gigabyte and is volatile.

flowchart TB
  subgraph Memory Hierarchy Speed vs Capacity Pyramid
    L1["1. CPU L1/L2 Cache (0.5 - 1 ns) | Bytes"]
    RAM["2. Main RAM / Redis Cache (0.1 - 1 ms) | Gigabytes"]
    SSD["3. Local NVMe SSD Disk (2 - 10 ms) | Terabytes"]
    RemoteDB["4. Distributed Database Cluster (15 - 50 ms) | Petabytes"]
  end
  
  L1 --> RAM --> SSD --> RemoteDB

Figure 1: The computer hardware memory pyramid illustrating speed, latency, and capacity trade-offs.

Hardware Latency Comparison Table

Storage LayerAccess MediumTypical LatencyCost per GBVolatilityPrimary Purpose
CPU L1/L2 CacheOn-Die SRAM$0.5 - 1.0\text{ ns}$ExtremeVolatileCPU instruction execution.
Main RAM / RedisSystem DRAM$100 - 500\text{ }\mu\text{s}$ ($0.1-0.5\text{ms}$)High ($\sim\$5/\text{GB}$)VolatileHigh-speed hot data cache.
Local NVMe SSDFlash Memory$2.0 - 10.0\text{ ms}$Moderate ($\sim\$0.10/\text{GB}$)Non-VolatilePersistent database storage.
Distributed DatabaseNetwork + SSD$15.0 - 50.0\text{ ms}$ModerateNon-VolatilePrimary transactional truth.

Core Mechanics: Cache Hits, Cache Misses, and Hit Ratio

When an application queries a cache layer, two outcomes occur:

flowchart TD
  App[Application Request] --> CacheCheck{Is Key in Cache?}
  
  CacheCheck -->|YES: Cache Hit| HitPath["Cache Hit (Sub-ms)<br/>Return Cached Data Immediately!<br/>Latency: 1.5 ms"]
  CacheCheck -->|NO: Cache Miss| MissPath["Cache Miss<br/>Fetch from Database (45 ms)<br/>Write to Cache & Return"]
  
  MissPath --> DB[(Primary Database)]
  DB --> WriteCache[Write Payload to Cache with TTL]

Figure 2: Flowchart depicting Cache Hit vs Cache Miss execution branches.

1. Cache Hit

The requested key exists in the cache and is un-expired. The application reads the payload directly from RAM in sub-milliseconds, bypassing the primary database.

2. Cache Miss

The requested key is missing from the cache or has expired. The application must query the primary database, return the payload to the caller, and asynchronously update the cache layer.

3. Cache Hit Ratio ($H$)

The **Cache Hit Ratio** is the percentage of total read requests satisfied directly by the cache layer:

$$H = \frac{\text{Total Cache Hits}}{\text{Total Cache Hits} + \text{Total Cache Misses}}$$


Mathematical Formulation: Average Memory Access Time (AMAT)

The overall average response latency experienced by an application is governed by the Average Memory Access Time (AMAT) equation:

$$\text{AMAT} = T_{\text{hit}} + (1 - H) \times M_{\text{penalty}}$$

Where:


Concrete Calculation Example


If a platform processes 10,000 RPS with $T_{\text{hit}} = 1.5\text{ms}$, $M_{\text{penalty}} = 45\text{ms}$, and $H = 95\%$:

$$\text{AMAT} = 1.5\text{ms} + (1 - 0.95) \times 45\text{ms} = 1.5\text{ms} + (0.05 \times 45\text{ms}) = 1.5\text{ms} + 2.25\text{ms} = 3.75\text{ms}$$

Compare this to an un-cached database system where average latency is $45\text{ms}$:



Principles of Locality: Temporal vs. Spatial Locality

Caching works because computer data access patterns exhibit two predictable behavioral properties:

flowchart TD
  Locality[Principles of Locality] --> Temporal[1. Temporal Locality]
  Locality --> Spatial[2. Spatial Locality]
  
  Temporal --> TDesc["If data is accessed ONCE, it is likely to be accessed AGAIN soon.<br/>Example: Trending breaking news article or hot product page."]
  Spatial --> SDesc["If data item A is accessed, nearby items A+1, A+2 are likely to be accessed soon.<br/>Example: Fetching user profile + profile settings + avatar."]

Figure 3: Comparison of Temporal vs Spatial Locality principles.


Access Pattern: The Cache-Aside (Lazy Loading) Workflow

The most ubiquitous caching pattern in distributed microservices is Cache-Aside:

sequenceDiagram
    autonumber
    actor Client as Application Client
    participant Cache as Redis In-Memory Cache
    participant DB as PostgreSQL Database
    
    Client->>Cache: 1. GET product:10492
    alt Cache Hit (95% of traffic)
        Cache-->>Client: 2. Return JSON Payload (1.5ms)
    else Cache Miss (5% of traffic)
        Cache-->>Client: 3. Return Key Not Found (Nil)
        Client->>DB: 4. SELECT * FROM products WHERE id=10492
        DB-->>Client: 5. Return SQL Record (45ms)
        Client->>Cache: 6. SETEX product:10492 3600 JSON_Payload (TTL: 1 Hour)
        Client-->>Client: 7. Process Request
    end

Figure 4: Sequence diagram of Cache-Aside lazy loading workflow.


Complete Worked Example: Go Thread-Safe In-Memory Cache with TTL

Let's inspect a complete Go implementation of a thread-safe in-memory cache featuring TTL expiration for the CacheLab platform (cachelab.com).

package main

import (
"context"
"fmt"
"sync"
"time"
)

type cacheItem struct {
value interface{}
expiration time.Time
}

type MemoryCache struct {
mu sync.RWMutex
items map[string]cacheItem
}

func NewMemoryCache(cleanupInterval time.Duration) *MemoryCache {
c := &MemoryCache{
items: make(map[string]cacheItem),
}

go c.startJanitor(cleanupInterval)
return c
}

func (c *MemoryCache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()

c.items[key] = cacheItem{
value: value,
expiration: time.Now().Add(ttl),
}
}

func (c *MemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()

item, exists := c.items[key]
if !exists {
return nil, false
}

// Check TTL Expiration
if time.Now().After(item.expiration) {
return nil, false
}

return item.value, true
}

func (c *MemoryCache) startJanitor(interval time.Duration) {
ticker := time.NewTicker(interval)
for range ticker.C {
c.mu.Lock()
now := time.Now()
for k, v := range c.items {
if now.After(v.expiration) {
delete(c.items, k)
}
}
c.mu.Unlock()
}
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. Un-Bounded In-Memory Cache GrowthKeys written to in-memory map without TTL expiration or max memory caps.Server runs out of RAM, triggering Linux kernel Out-Of-Memory (OOM) process kill.Memory utilization linearly increases to 100% until server crashes.Enforce strict TTL Expiration and LRU Eviction Caps on all cache instances.
2. Low Cache Hit Ratio ($H < 50\%$)Caching non-temporal, low-cardinality data or setting TTLs too short.High database CPU saturation despite running large cache clusters.Datadog/Prometheus metric shows Cache Hit Ratio below 50%.Audit cached key selection; cache only Hot Read-Heavy Data.
3. Stale Data InconsistencyPrimary database updates record but cached entry remains until TTL expires.Users see outdated profile information or incorrect prices after updating.Customer support tickets regarding outdated page details.Implement explicit Cache Invalidation on Database Writes (Cache Invalidate).
4. Cold Cache Server Boot StampedeNewly booted cache instance starts with 0 keys; 100% of traffic misses to DB simultaneously.Database crashes instantly upon deployment or restart of cache servers.Database CPU spikes to 100% immediately following cache service restart.Execute Pre-Warming Scripts to populate hot keys before shifting production traffic.

What You Should Remember

  1. Caching offloads RAM over disk: RAM reads take sub-milliseconds ($\sim 0.1-1.5\text{ms}$) compared to disk/network queries ($\sim 15-50\text{ms}$).
  2. Hit Ratio ($H$) governs AMAT latency: Higher hit ratios drastically lower Average Memory Access Time ($\text{AMAT} = T_{\text{hit}} + (1-H) \times M$).
  3. Exploit Temporal and Spatial Locality: Cache data accessed recently and data likely to be accessed together.
  4. Always set explicit Time-To-Live (TTL): Prevent memory exhaustion by setting TTL expiration bounds on all cached keys.
  5. Pre-warm cold caches before shifting traffic: Prevent cold boot database crashes by pre-loading hot keys into new cache instances.

Cold Cache Mitigation & Pre-Warming

When a cache node restarts or is freshly deployed, its RAM is completely empty. If incoming production traffic is routed immediately to a cold cache, $100\%$ of requests miss, causing a massive database load spike that can crash primary databases. To prevent cold cache outages, systems execute **Cache Pre-Warming**:
  1. Synthetic warm-up scripts query top 1,000 most frequently accessed keys from database replicas.
  2. The results are pre-populated into the cache layer before registering the node with load balancers.
  3. Live production traffic is shifted only after the Cache Hit Ratio exceeds $90\%$ in pre-production health checks.

Multi-Tiered Caching (L1 In-Memory + L2 Redis)

High-performance architectures deploy **Multi-Tiered Caching**: When an API request arrives, the application checks L1 local memory first. On an L1 miss, it checks L2 Redis. On an L2 miss, it queries the primary SQL database. This multi-tiered strategy protects Redis from hot-key single-node saturation while maintaining global cache coherence.

Cache Stampede Early Expiration (XFetch)

When a hot key expires in L2 cache, hundreds of application pods attempt to query the SQL database simultaneously. To prevent thundering herds, application SDKs use **Probabilistic Early Expiration (XFetch)**: as a key approaches its TTL expiration, reads probabilistically trigger an asynchronous background database refresh before the key actually expires, guaranteeing $100\%$ cache hits for active keys.
TermDefinition
CacheA temporary high-speed data storage layer (RAM) used to serve data faster than the primary storage.
Cache Hit Ratio ($H$)The percentage of total read requests served directly by the cache layer.
AMAT (Average Memory Access Time)The mathematical expected latency across combined cache hits and cache misses.
Temporal LocalityThe principle that data accessed recently is likely to be accessed again in the near future.
Cache-AsideAn access pattern where the application reads from cache first, and on miss fetches from DB and writes to cache.
TTL (Time-To-Live)The expiration duration after which a cached key is automatically evicted.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing the caching tier for an online news platform (`news.cachelab.com`): **Questions**:
  1. Calculate the Average Memory Access Time (AMAT) if the Cache Hit Ratio is $98\%$.
  2. Formulate the Cache-Aside TTL strategy for breaking news articles vs archival articles.

Interactive Self-Assessment

A 98% hit ratio reduces database query load from 20% of traffic down to 2%, achieving a 10x reduction in database CPU queries.

A 98% hit ratio automatically converts relational database schemas into NoSQL tables.

A 98% hit ratio revokes client HTTPS TLS encryption certificates.

A 98% hit ratio doubles the physical hardware clock speed of CPU cores.

The cache map grows monotonically over time, exhausting server RAM and triggering Out-Of-Memory (OOM) process kills.

The cache server automatically changes public DNS nameserver records.

The cache server converts SQL database tables into un-indexed CSV files.

Un-bounded caches cause physical electrical damage to network router cables.


What to Learn Next

Track: Data, Storage and Messaging

Previous: Cache Stampede — When Expiry Melts the Database

Next: Caching Strategies — Aside, Through, Behind, and Refresh-Ahead

Series: Caching

  1. Caching 101 — Memory Offloading and Latency Reduction (this guide)
  2. Caching Strategies — Aside, Through, Behind, and Refresh-Ahead
  3. Cache Eviction Policies — LRU, LFU, TTL, and Friends
  4. Distributed Caching — Sharding and High-Availability Clusters
  5. Cache Stampede — When Expiry Melts the Database
  6. Stale Cache After Write — When Your Own Update Disappears
  7. Content Delivery Networks (CDN) — Edge Acceleration and Caching
  8. Bloom Filters — Probabilistic Set Membership at Scale

All series

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab