Cache levels
“Levels” of cache means the layers a request can be answered at, on its way from the user to the database. Each layer catches some requests, so fewer reach the next one. The names L1/L2 are borrowed from CPU caches (L1, L2, L3, then RAM). The idea is the same: small and fast close to whoever uses the data, big and slower further away.
phone/browser ─▶ CDN edge ─▶ edge proxy ─▶ app server memory ─▶ distributed cache ─▶ database (client) (edge) (gateway) (local, "L1") (shared, "L2") (its own caches)| Level | Example | Read latency | Size | Shared by | Main problem |
|---|---|---|---|---|---|
| Client | App’s local database, browser HTTP cache | ~0 (no network) | MB–GB per device | One user | You can’t reach in and clear it |
| CDN / edge | Cloudflare, CloudFront | ~10–30 ms from the user, vs 100+ ms to a distant origin | Huge, disk-backed | Everyone near that site | Only for public content, or content that’s the same for many users |
| Edge proxy / gateway | Nginx or Varnish response cache | ~1 ms within the site | GB | All users through that proxy | Must know which responses are safe to share |
| In-process (L1) | A map inside each app server | ~100 ns | MB to a few GB | One server | Every server has its own copy, and copies disagree |
| Distributed (L2) | Redis, Memcached | ~0.5 ms (one network round trip) | TB across many nodes | All app servers | Network hop, partitioning, node failures |
| Database’s own | Buffer pool, OS page cache | ~1 ms from memory, more from disk | Server RAM | Automatic | Not under your control |
Latencies are orders of magnitude, not measurements.
Three rules
1. Each level only sees what the level before it missed, so hit rates multiply.
L1 hit rate 80% → 20% of reads go to the distributed cacheL2 hit rate 95% → 20% × 5% = 1% reach the databaseThis is why a small L1 is so effective against a hot key. Each app server answers it from its own memory, so the one cache node holding it isn’t swamped.
2. The closer to the user, the more copies, and the harder it is to invalidate.
- Database: one copy.
- Distributed cache: one per key, plus replicas.
- L1: one per app server, possibly hundreds.
- Devices: one per device, possibly millions.
Each level stays stale until its TTL runs out or an invalidation reaches it. So the near levels get short TTLs (L1: seconds), and only data that may be stale goes far out.
3. Staleness adds up along the path. An L1 entry with a 10 s TTL, filled from a distributed-cache entry that was already 60 s old, can show data 70 s old. To answer “how stale can it get?”, add up the TTLs along the path.
Worked example: a messaging app
| Data | Where it’s cached | Why |
|---|---|---|
| Contacts’ profiles | On the phone, refreshed when a profile changes | Rarely changes; the phone needs it on every screen |
| Profile photos | CDN, long TTL, new URL when the photo changes | Large and rarely changes; a new URL avoids invalidating the CDN |
| Group membership | Owner server’s memory (L1), plus the shared cache (L2) for loading | Checked on every message; must not be stale, so one authoritative copy decides (consistency) |
| Everything | Database | Source of truth |
What interviewers ask about levels
- “Why not just make Redis bigger?” Every request still pays a network round trip, and a hot key still lands on one node. L1 copies avoid both.
- “A user changes their name. How long until everyone sees it?” The sum of the TTLs along the path, or however quickly your invalidations reach each level.
- “Which data would you keep out of the far levels?” Anything where staleness is a correctness or privacy bug, such as permissions and membership. Keep it out of long-TTL levels, or invalidate it on every change.