*Cache

September 15, 2025

What is caching?

Caching stores the result of expensive operations (I/O/CPU/network) closer to where it’s consumed so future requests are served faster and cheaper.

Layers

  • Client/browser, CDN/edge, reverse proxy, application cache (Redis/memory), database/materialized views.

Where to place the cache

  • Client, CDN/edge, proxy, application tier, database (materialized views/indices).

Invalidation

  • TTL, write-through/back, cache-aside; versioned keys, ETag/If-None-Match.

Consistency

  • Stale-while-revalidate, read-through; beware thundering herd → locks/adaptive TTL.

Code: cache-aside with Redis (Node.js)

// language-javascript
import Redis from 'ioredis'
const redis = new Redis(process.env.REDIS_URL)

async function getUser(userId) {
  const key = `user:${userId}`
  const cached = await redis.get(key)
  if (cached) return JSON.parse(cached)
  const user = await db.users.findById(userId) // expensive
  await redis.set(key, JSON.stringify(user), 'EX', 60) // TTL 60s
  return user
}

Code: HTTP caching headers

# language-nginx
location /static/ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

location /api/resource {
  add_header Cache-Control "public, max-age=60, stale-while-revalidate=120";
  proxy_pass http://app;
}

Stampede protection (single-flight)

// language-typescript
const inflight = new Map<string, Promise<any>>()

export async function getWithSingleFlight(key: string, fetcher: () => Promise<any>) {
  const cached = await redis.get(key)
  if (cached) return JSON.parse(cached)
  if (inflight.has(key)) return inflight.get(key)
  const p = (async () => {
    try {
      const val = await fetcher()
      await redis.setex(key, 60, JSON.stringify(val))
      return val
    } finally {
      inflight.delete(key)
    }
  })()
  inflight.set(key, p)
  return p
}

Write-through vs cache-aside

  • Cache-aside: application reads DB and populates cache. Simple, but stale if writes bypass.
  • Write-through: writes go through cache which updates DB. Lower staleness, more coupling.
  • Write-back: write to cache, flush to DB asynchronously; higher risk, great for high write rates with durability safeguards.

Versioned keys for busting

// language-javascript
function userKey(id, v) { return `user:${id}:v${v}` }
// Bump version on write to bust all readers atomically
async function updateUser(id, patch) {
  const v = await redis.incr(`user:${id}:ver`)
  const data = await db.updateUser(id, patch)
  await redis.set(userKey(id, v), JSON.stringify(data), 'EX', 300)
}

Materialized views (DB)

-- language-sql
CREATE MATERIALIZED VIEW top_articles AS
SELECT article_id, count(*) AS views
FROM page_views
GROUP BY article_id;

-- Refresh off-peak or via CDC triggers
REFRESH MATERIALIZED VIEW CONCURRENTLY top_articles;

CDN + app cache layering

  • Public immutable assets: year-long TTL with content hashing.
  • API GETs: CDN s-maxage with SWR; app cache 30–120s TTL.
  • Personalized data: short TTL + ETag validators.

Metrics and alerts

  • Hit ratio per tier, origin offload, p95 fetch time, stampede rate.
  • Cache key cardinality (avoid explosive dimensions), memory usage, eviction rate.