Caching and Content Delivery - Complete Notes
A language-neutral guide to placing reusable copies of data safely, reasoning about freshness and invalidation, protecting origins from bursts, operating multi-layer caches, and explaining the trade-offs clearly in production and system-design interviews.
00. The cache mental model
A cache is a faster, limited, and disposable copy of data whose authority still belongs somewhere else.
Think of a busy restaurant. The pantry near the cook holds ingredients used often, while the warehouse holds the full stock. The pantry shortens each trip, but it has limited space and its labels can become wrong when the warehouse changes. A useful pantry needs rules for what enters, how long it remains trusted, what leaves first, and what happens when it is empty.
Caching exchanges one kind of work for another. It avoids repeated computation, storage reads, or network transfers, but introduces copies, freshness rules, memory pressure, coordination, and new failure modes. A cache is valuable only when the avoided cost is larger than the cost of managing those copies.
| Term | Precise meaning | Design question |
|---|---|---|
| Source of truth | The authoritative system allowed to decide the current value | Where must writes commit before success is acknowledged? |
| Cache entry | A value plus identity, freshness, metadata, and sometimes validation state | Can two users or versions ever share this identity safely? |
| Hit | A request satisfied from a reusable cached entry | Was it fresh, validated, or deliberately stale? |
| Miss | No reusable entry exists, so the underlying path must run | Can the source absorb simultaneous misses? |
| Freshness | The period or condition under which a copy may be reused | What staleness can this business operation tolerate? |
| Invalidation | Making a cached representation unusable after authoritative state changes | What happens if the invalidation message is delayed or lost? |
| Eviction | Removing an entry to reclaim limited capacity | Which entry has the lowest expected future value? |
| Warm cache | A cache containing much of the active working set | How is capacity protected during restart or failover? |
Define ownership, key identity, allowed staleness, and failure behavior before selecting a cache product. "Put Redis in front" or "add a CDN" is not a complete design.
01. Why caches work
Caches work when future access is predictable enough that a small, fast layer can serve a large share of requests.
Locality and working sets
- Temporal locality
- Something used recently is likely to be used again soon, such as a trending product.
- Spatial locality
- Items near a requested item are likely to be requested, such as adjacent bytes in a file.
- Frequency skew
- A small set receives a large fraction of traffic, such as popular profile pages.
- Working set
- The data actively needed during a relevant period, not the total stored dataset.
If a 10 TB catalog has a 4 GB hourly working set, a 6 GB cache may perform well. If every request chooses a random object uniformly from 10 TB, the same cache will churn and provide little value. Measure reuse-distance distributions and key popularity instead of assuming a universal percentage of the database belongs in memory.
Hit ratio, byte hit ratio, and miss cost
request_hit_ratio = cache_hits / cache_lookups
byte_hit_ratio = bytes_served_from_cache / total_bytes_served
average_latency =
hit_ratio * hit_latency
+ (1 - hit_ratio) * miss_latency
origin_requests_per_second =
total_requests_per_second * (1 - hit_ratio)
A request hit ratio treats a 1 KB response and a 1 GB video equally. A CDN often cares about byte hit ratio because bandwidth and origin egress dominate cost. An application cache may care more about avoided database CPU or tail latency. Count hits only when they satisfy correctness. Serving the wrong tenant's value is not a successful hit.
| Example | Without cache | With 95% hit ratio |
|---|---|---|
| 20,000 reads/s, source takes 40 ms | 20,000 source reads/s | About 1,000 source reads/s in steady state |
| Cache hit takes 2 ms | Average near 40 ms | Average near 3.9 ms before queueing effects |
| Cache becomes empty | No behavior change | Source load can jump 20 times immediately |
Misses are usually slower than hits and may queue behind other misses. Track hit and miss latency separately at p50, p95, p99, and p99.9. A high overall hit ratio can still hide a severe hot-key or cold-start tail.
02. Cache layers and placement
Placement decides latency, sharing, failure scope, consistency, and who controls the copy.
| Layer | Useful for | Main limitation |
|---|---|---|
| CPU and operating-system cache | Instructions, memory pages, filesystem blocks | Mostly implicit and local to one machine |
| Browser or mobile client | Static assets, HTTP responses, offline data | Hard to purge and physically outside server control |
| Process-local application cache | Small computed values, metadata, configuration | Duplicated across instances and lost on restart |
| Distributed application cache | Shared objects, sessions when appropriate, expensive query results | Network hop, serialization cost, shared failure domain |
| Database cache | Pages, plans, materialized results, indexes | May still consume database CPU and connections |
| Reverse proxy | Whole HTTP responses near the application | Cache key and authentication mistakes affect many users |
| Edge cache or CDN | Static and explicitly cacheable dynamic content near users | Globally distributed invalidation and privacy risk |
Multi-level caching is common: browser, edge, regional shield, application cache, then database buffer cache. Each layer should have a named owner, observable behavior, and a freshness policy. More layers can reduce latency and origin traffic, but make incident diagnosis and invalidation harder. An entry might be absent in one layer, stale in another, and fresh in a third.
Private and shared caches
A private cache serves one user or process. A shared cache serves many users. The distinction is a security boundary. A response personalized by cookies, authorization, locale, experiment, or tenant must not enter a shared cache unless every relevant dimension is represented safely in the cache key and the response is explicitly allowed to be shared.
Content delivery networks
A CDN stores reusable HTTP objects at geographically distributed points of presence. A nearby edge reduces round-trip time and origin bandwidth. A tiered topology can let an edge miss consult a regional or upper-tier cache before the origin, concentrating origin connections and reducing duplicate fills. It does not remove the need for origin capacity, because a purge, expiry wave, routing shift, or new popular object can still create misses.
03. Read caching patterns
Cache-aside, or lazy loading
The application checks the cache, reads the source on a miss, and places the result in the cache. It is simple and keeps the cache focused on requested data.
READ(key):
cached = cache.get(key)
if cached exists and cached.version is acceptable:
return cached.value
value = source.read(key)
cache.put(key, value, ttl = policy_for(value))
return value
Client -> Service -> Cache
| hit -> value
| miss
v
Source -> value -> Cache -> Client
The race is important. A reader can miss, read old source state, then populate the cache after a concurrent writer invalidates it. Version checks, delete-after-write ordering, short TTLs, or a second invalidation can bound the problem. For strict correctness, read directly from the authority or use a cache protocol that validates versions.
Read-through
The cache abstraction loads from the source when a key is absent. Callers see one interface. This centralizes loading, TTL, and coalescing, but the loader must preserve source authorization, cancellation, timeouts, and error semantics. A generic cache library cannot guess business freshness.
Memoization
Memoization caches a function result by its inputs. It is safe when the function is deterministic for the complete key. Hidden inputs such as current time, feature flags, permissions, model versions, locale, and database state must either enter the key or make the result non-cacheable. Bound memoization by size and lifetime. An unbounded map is a memory leak with a convenient name.
Negative caching
Negative caching stores "not found," empty, or known failure results briefly. It protects the source from repeated requests for absent keys and automated enumeration. Use a shorter TTL than for existing data, because a missing object can be created. Distinguish authoritative absence from temporary source failure. Never cache an authorization failure under a key shared by other users.
04. Write caching and consistency patterns
| Pattern | Write flow | Benefit | Primary risk |
|---|---|---|---|
| Write-through | Write cache layer, which synchronously writes authority | Cache is warm after acknowledged writes | Added write latency and cache dependency |
| Write-around | Write authority, then invalidate or bypass cache | One-time writes do not pollute cache | Next read misses |
| Write-behind | Accept into cache or queue, persist later | Low latency and batchable writes | Data loss, reordering, and complex recovery |
| Refresh-ahead | Refresh popular entries before expiry | Fewer user-visible misses | Refreshes unused data and can overload source |
Write-behind makes the cache or durable queue part of the authoritative write path. It is no longer "just a cache." Acknowledged writes need replication, durable logging, idempotent replay, ordering rules, backpressure, and reconciliation. If losing an accepted payment or inventory update is unacceptable, a volatile cache is not sufficient.
A safe write and invalidation flow
UPDATE_PRODUCT(command):
begin source transaction
update product, increment version
append cache-invalidation event to transactional outbox
commit
best_effort cache.delete(product_key)
return committed version
RELAY:
repeatedly publish unprocessed outbox events
INVALIDATION_CONSUMER:
delete cache key or reject versions older than event.version
acknowledge event only after the cache action succeeds
The direct deletion gives low propagation latency. The transactional outbox repairs a lost deletion after a crash. TTL is the final bound if both paths fail. A version in the cached value prevents an old fill from overwriting a newer one when the cache supports compare-and-set or the application checks versions.
| Failure point | State | Recovery |
|---|---|---|
| Source transaction fails | No committed change and no valid outbox event | Return failure; do not invalidate as if write succeeded |
| Process crashes after commit | Source is new, cache may be old | Outbox relay invalidates; TTL bounds delay |
| Invalidation delivered twice | Same key deleted repeatedly | Deletion is idempotent |
| Old miss fill races new write | Old value may be reinserted | Versioned put, second delete, or short TTL |
| Cache unavailable | Source remains authoritative | Choose bounded source fallback or fail closed by domain |
05. Freshness, TTL, validation, and invalidation
A TTL is a business staleness decision expressed as time, not a correctness mechanism by itself.
TTL and time to idle
- Time to live, or TTL
- Expire a fixed duration after creation or refresh, whether or not the entry is read.
- Time to idle, or TTI
- Expire after the entry has not been accessed for a duration.
- Absolute expiry
- Expire at a specified instant, such as the end of a pricing promotion.
Choose TTL from the harm caused by stale data, update frequency, source load, and recovery needs. Product descriptions might tolerate minutes. Authorization revocation may tolerate seconds or no shared caching at all. A longer TTL raises hit ratio and outage tolerance but lengthens stale exposure. Add random jitter so millions of entries do not expire simultaneously.
effective_ttl = base_ttl * random_between(0.8, 1.2)
Version validation
Validation asks whether a stored representation is still current. HTTP uses validators such as
ETag with If-None-Match, or Last-Modified with
If-Modified-Since. A 304 Not Modified response avoids retransmitting the
body while confirming reuse. Application caches can store entity versions, revision numbers, or
content hashes. Validation still costs a round trip, but may save computation and bandwidth.
Versioned keys and tagging
A versioned key such as asset:sha256:... or catalog:v42:item:123 turns
invalidation into publishing a new name. Immutable assets can receive long lifetimes because old
URLs still identify old bytes. The application must switch references atomically enough for its
consistency needs, and old versions still require eventual cleanup.
Tags group entries by dependency, such as product:123 or
tenant:acme:catalog. Tag invalidation is operationally convenient, but the reverse
index consumes memory and a broad purge can cause a miss storm. Prefer precise tags and stage
large invalidations.
Serving stale intentionally
Stale-while-revalidate serves a stale value immediately while one controlled refresh happens in the background. Stale-if-error serves stale data when the source fails. Both trade freshness for availability and lower tail latency. State explicit maximum stale windows, exclude safety-critical values, preserve authorization, and expose stale age to operators.
"The cache was stale" is not a full incident explanation. Ask whether the age was within policy, whether the client was told, whether the source was healthy, and whether the represented data was eligible for stale service.
06. HTTP caching and cache keys
HTTP already defines a distributed cache protocol. Use its semantics before inventing custom headers.
| Directive or field | Meaning | Common misunderstanding |
|---|---|---|
max-age |
Freshness lifetime in seconds for receiving caches | It is relative to response generation, not a purge guarantee |
s-maxage |
Shared-cache freshness overriding max-age |
It does not set browser freshness |
public |
Allows storage by shared caches where otherwise restricted | It does not make private data safe to share |
private |
Response is intended for a private cache, not a shared cache | It does not encrypt the response |
no-cache |
Stored response requires validation before reuse | It does not mean "never store" |
no-store |
Caches must not store the response under normal HTTP rules | It cannot erase copies already leaked elsewhere |
must-revalidate |
Stale response cannot normally be reused without successful validation | It does not force validation while the entry is fresh |
Vary |
Names request fields that affect representation selection | Unbounded variation can destroy hit ratio |
Age |
Approximate seconds since generation or successful validation | It is not application object age |
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30
ETag: "product-123-v42"
Vary: Accept-Encoding
The cache key commonly begins with method, target URI, and selected request fields. Query normalization, host, scheme, headers, cookies, compression, locale, device variants, experiments, and authorization can change the representation. If a representation varies but the key does not, users receive wrong content. If the key varies on noisy data that does not affect the response, the cache fragments and hit ratio falls.
What should be cacheable?
- Cache immutable, content-addressed assets for a long time.
- Cache public catalog responses under an explicit freshness and purge policy.
- Use private caching or no-store for personal financial, medical, or secret-bearing responses.
- Do not assume only status 200 is cacheable. Define negative caching deliberately.
- Do not cache unsafe method responses as if they were reusable reads.
- Do not share responses merely because an authentication header was stripped at the edge.
07. Capacity and eviction algorithms
Eviction decides which copy to sacrifice when limited memory cannot hold the working set.
| Policy | Idea | Works well when | Weakness |
|---|---|---|---|
| LRU | Evict least recently used | Recent use predicts near-future use | A large scan can evict a valuable working set |
| LFU | Evict least frequently used | Stable popularity matters | Old popularity needs decay to avoid permanent winners |
| FIFO | Evict oldest insertion | Low overhead matters more than precision | Ignores reuse |
| Random | Evict a random candidate | Access is uniform or metadata cost must be tiny | May discard a very hot entry |
| TTL-first | Prefer entries closest to expiry | Application expiry predicts value | Popularity is ignored |
| Size or cost aware | Balance bytes, recomputation cost, and reuse | Objects vary greatly in size or miss cost | Needs better metadata and tuning |
Production systems often approximate policies by sampling because exact global LRU or LFU metadata is expensive. Measure evictions, rejected writes, memory fragmentation, object-size distribution, and hit ratio by key family. Reserve headroom for metadata, replication buffers, network buffers, and allocator fragmentation. A configured 10 GB data limit does not mean the process needs only 10 GB of memory.
Admission is different from eviction
Eviction chooses what leaves. Admission chooses whether a new object deserves space at all. A single-use scan should often bypass a small cache, preserving frequently reused items. Size limits prevent a single large object from evicting thousands of valuable small objects. Separate caches or quotas can isolate workloads with different value and access patterns.
08. Stampedes, penetration, and hot keys
Cache stampede
A stampede occurs when many callers miss or observe expiry together and all perform the expensive fill. The cache normally hides source capacity limits, so a cold cache can turn a healthy source into an overloaded source.
GET_OR_LOAD(key):
value = cache.get(key)
if fresh(value): return value
if this process owns refresh(key):
loaded = source.read(key, deadline)
cache.put(key, loaded, jittered_ttl)
publish result to waiters
return loaded
wait briefly for owner
if fresh value appears: return it
if eligible stale value exists: return stale
otherwise fail within caller deadline
Single-flight or request coalescing allows one fill per key while other requests wait or receive stale data. It must be bounded by deadlines. A failed leader cannot hold every waiter forever. Distributed locks can coordinate across processes, but they add their own failure and fencing problems. Often per-node coalescing plus source concurrency limits is simpler and sufficient.
Probabilistic early refresh
Refreshing every entry at a fixed threshold still synchronizes clients. A probabilistic policy raises the chance of refresh as expiry approaches. Popular keys receive more opportunities to refresh early, while cold keys usually expire naturally. Only one winner should perform the refresh.
remaining = expires_at - now
refresh_probability = clamp(refresh_window / max(remaining, 1), 0, 1)
if random() < refresh_probability and acquire_refresh_ownership(key):
refresh(key)
Cache penetration
Penetration is repeated access to keys that never become cache hits, often due to invalid input, enumeration, or random identifiers. Validate syntax early, rate-limit abusive principals, use short negative caching for authoritative absence, and consider probabilistic membership filters where false positives are acceptable. Do not let a filter become the source of truth, because a stale false negative could hide a real object.
Hot keys
One extremely popular key can saturate a cache shard, network path, or serialization thread even when overall capacity looks healthy. Replicate read-only hot values, use near-caches, split large aggregates, cache pre-serialized forms, and apply per-key request coalescing. Detect hot keys by sampled access counts and shard imbalance without logging sensitive raw keys.
09. Large objects and content delivery
Large objects change the unit of value. Cache chunks or byte ranges when clients consume only parts, support resumable transfer, and avoid buffering a complete object in application memory. Compression saves bandwidth but costs CPU and can fragment variants. Precompress immutable assets when practical and include content encoding in representation selection.
| Decision | Trade-off |
|---|---|
| Whole-object caching | Simple, but one large object can evict many smaller high-value entries |
| Chunk caching | Improves partial reads and retries, but increases key count and metadata |
| Edge delivery | Reduces distance and origin egress, but distributes invalidation globally |
| Signed URLs | Offloads delivery, but expiry, cache key, and authorization scope must align |
| Origin shield | Collapses regional misses, but becomes another dependency and concentration point |
10. Security, privacy, and tenant isolation
A shared cache amplifies both performance and mistakes.
Cache poisoning and deception
Cache poisoning stores an attacker-influenced response under a key later used by victims. Unkeyed request headers, host manipulation, query normalization differences, response splitting, and inconsistent proxy parsing are common ingredients. Cache deception tricks a cache into storing a private dynamic response by making its path look static.
- Canonicalize and validate scheme, host, path, query, and forwarding headers consistently.
- Include every representation-changing input in the key, or reject it before caching.
- Ignore attacker-controlled headers that should not influence origin output.
- Do not cache authenticated responses by file extension or path shape alone.
- Test duplicate headers, ambiguous paths, encoded separators, and parameter reordering.
- Restrict purge permissions, log them, and protect them from replay.
Tenant and authorization boundaries
Prefix application keys with stable tenant identity and object version, not a display name. Authorize before returning a cached object unless the object is independently public. Do not put raw bearer tokens in keys or logs. If role or policy affects the representation, encode a stable authorization-policy version or keep the result private. Prevent one tenant from consuming all memory through quotas, admission controls, and size limits.
Sensitive data and retention
Encryption in transit and at rest protects some threats but does not fix cross-user reuse. Minimize personal data, align cache lifetime with deletion and residency obligations, and ensure backups or persistent tiers do not silently extend retention. A purge API should cover every relevant layer. For secrets, payment details, health records, and similarly sensitive views, bypass shared caches unless a reviewed design proves safe.
11. Failure policy: fail open, fail closed, or bypass
| Domain | Cache failure response | Reason |
|---|---|---|
| Public product description | Bounded stale response or source fallback | Availability usually outweighs brief staleness |
| Authorization revocation | Fail closed or validate against authority | Stale allow decisions can expose protected data |
| Rate-limit counter | Domain-specific conservative limit | Fail open invites abuse; fail closed can cause outage |
| Recommendation feed | Fallback to popular or older safe content | Degraded personalization is acceptable |
| Inventory availability | Use cache as hint, verify before commitment | Overselling has a business correctness cost |
Source fallback must be capacity-limited. If every cache client immediately bypasses a failed cache, the source can fail next. Use concurrency limits, deadlines, load shedding, and a degraded response. Cache errors should not be retried recursively through every layer.
12. End-to-end cached catalog design
Assume 50,000 product reads/s at peak, 100 product updates/s, a 300 ms acceptable public staleness target for prices, and a requirement to verify inventory during checkout.
Browser private cache
|
v
CDN edge cache --> regional shield
| miss
v
Catalog API --> process near-cache --> distributed cache
| miss
v
authoritative database
Checkout API always validates current price version and inventory before commitment.
Read path
- The public response key includes product ID, locale, currency, and representation version.
- The edge serves a fresh public response or revalidates through the shield.
- The API checks a tiny process-local cache, then the distributed cache.
- A miss is coalesced per product and loaded with a deadline from the database.
- The value stores product version, source commit time, and cache creation time.
- The API returns an ETag derived from the representation version.
Write and invalidation path
- An authorized command updates the product and increments its version transactionally.
- The same transaction appends an invalidation event.
- The writer performs a best-effort distributed-cache delete after commit.
- The relay publishes the event, and consumers delete near-cache and edge entries by tag.
- Consumers ignore older event versions and retry idempotently.
- A short price TTL bounds any missed invalidation; descriptions may have a longer TTL.
Capacity estimate
Suppose the active set is 2 million objects averaging 4 KB serialized. Raw values need about 8 GB. Add key, metadata, allocator, replication, and growth overhead rather than sizing at exactly 8 GB. With 95% application-cache hits, the database sees about 2,500 reads/s before edge hits. If the cache empties, the potential source demand is 50,000 reads/s, so warm-up must be rate-limited and the system must serve stale or shed load instead of assuming the database absorbs it.
13. Observability and troubleshooting
| Signal | Why it matters | Useful breakdown |
|---|---|---|
| Hit, miss, stale, bypass, and error rate | Separates successful reuse from all other outcomes | Layer, key family, region, tenant class |
| Hit and miss latency distributions | Shows whether the cache improves user experience | p50, p95, p99, p99.9 |
| Source request and error rate | Measures actual offload and source protection | Cache outcome and caller |
| Evictions, expirations, and rejected writes | Reveals capacity or policy pressure | Shard and policy |
| Memory, fragmentation, CPU, network, connections | Finds cache-server saturation | Node and shard |
| Coalesced waiters and load duration | Detects stampedes and stuck leaders | Key hash or safe key family |
| Entry age and invalidation lag | Measures real freshness, not configured TTL | Data class and region |
| Byte hit ratio and origin egress | Measures content-delivery cost reduction | Object size, media type, point of presence |
Use correlation IDs across cache, source, and invalidation paths. Sample keys by a one-way digest
or family to avoid exposing personal data. HTTP's standardized Cache-Status field can
describe forwarding and hit behavior, but exposing detailed keys publicly can aid attackers, so
restrict sensitive diagnostics.
Troubleshooting playbook
- Confirm whether users see wrong data, high latency, errors, or excess cost.
- Identify the affected layer, key family, tenant, region, and deployment version.
- Compare hit ratio with source load. A miss spike plus source saturation suggests cold cache.
- Check evictions, memory, skew, hot keys, and expiry waves.
- Inspect invalidation lag and compare cached version with source version.
- Mitigate with bounded stale service, fill limits, selective bypass, or precise purge.
- Avoid a global purge unless origin capacity has been modeled and protected.
- After recovery, replay invalidations and reconcile sampled keys against the source.
14. Production and deployment practices
- Assign separate TTL and failure policies by data class, not one global default.
- Use deterministic key schemas with explicit tenant, schema, and representation versions.
- Deploy key-schema changes with dual read or dual fill only for a bounded migration window.
- Canary cache-policy changes and watch source load, not only cache hit ratio.
- Warm only the proven hot set at a controlled rate. Avoid replaying the entire database.
- Distribute replicas and shards across failure domains without assuming replicas add capacity.
- Set cache timeouts below caller deadlines and bound connection pools and waiting queues.
- Keep an emergency bypass and purge mechanism protected by authorization and audit logging.
- Practice cache loss, partial shard failure, stale invalidations, and origin slowdown.
- Track cost per million requests and per cached GB, including network and operational cost.
15. Common mistakes and safer corrections
| Mistake | Why it fails | Safer correction |
|---|---|---|
| Using only object ID as key | Tenant, locale, version, or authorization variants collide | Define and test complete representation identity |
| Very long TTL with no invalidation repair | Lost invalidation causes long stale exposure | Durable invalidation plus TTL as a bound |
| Deleting cache before committing source | A reader can refill old state before the write commits | Commit source first, then invalidate, with race protection |
| No stampede protection | Expiry converts one expensive read into thousands | Jitter, coalescing, early refresh, and source limits |
| Unlimited source fallback | Cache outage becomes database outage | Bound concurrency and serve safe degraded results |
| Global purge during peak | Every layer misses together | Version keys or staged targeted invalidation |
| Unbounded local map | Memory grows until process failure | Bound weight, lifetime, and admission |
| Caching errors as absence | Temporary failure becomes a believable not-found result | Classify errors and negative-cache only authoritative absence |
| One shared cache for unrelated workloads | A scan or tenant can evict critical data | Quotas, admission, or separate capacity pools |
| Reporting one global hit ratio | Hot endpoints hide broken or unsafe key families | Measure outcome by layer, family, size, and region |
16. Testing strategy
Unit and property tests
- Prove key construction distinguishes tenants, locales, versions, permissions, and variants.
- Use a controllable clock to test fresh, stale, expired, and jitter boundaries.
- Verify negative results use different types and lifetimes from source errors.
- Property-test that canonical query inputs produce the intended key and no collisions.
- Test serialization compatibility across rolling deployments.
Integration and concurrency tests
- Run real cache and source services for TTL, eviction, reconnection, and atomic-operation tests.
- Race a cache miss against an update and verify old data cannot persist beyond policy.
- Send 1,000 simultaneous misses for one key and verify source calls remain bounded.
- Test duplicated, reordered, delayed, and lost invalidation events.
- Verify HTTP validators,
Vary,Age, and private/shared behavior. - Test cache poisoning inputs across application, proxy, and CDN parsing.
Load, fault, and recovery tests
- Measure warm and cold throughput, tail latency, byte hit ratio, and source offload.
- Restart the cache at peak test load and confirm controlled warm-up.
- Fail one shard, add latency, exhaust connections, and corrupt selected serialized entries.
- Expire a large key cohort together to expose synchronized refresh.
- Disable invalidation delivery, then restore it and verify replay and reconciliation.
- Run restore or regional failover while measuring stale age and origin capacity.
17. Hands-on exercises
Exercise 1: Key design review
Design keys for a product page varying by tenant, product, currency, locale, tax region, login state, experiment, and schema version. Decide which dimensions truly change the response. Expected reasoning: unnecessary dimensions reduce hit ratio, but omitting a real dimension causes cross-context data leakage. Prefer public base content plus separately private personalization when possible.
Exercise 2: Stampede simulator
Build a small approved-language service with a 100 ms source loader. Fire 500 concurrent requests after expiry. Compare no protection, one process-local single-flight, jitter, and bounded stale service. Expected reasoning: compare source-call count and tail latency, not only client success.
Exercise 3: Invalidation failure timeline
Write timelines for a writer crash before commit, after commit but before delete, and after delete but before event acknowledgement. Expected reasoning: source commit defines truth, direct deletion reduces normal latency, durable outbox retries repair crashes, and TTL bounds residual staleness.
Exercise 4: CDN policy
Define policies for fingerprinted JavaScript, a public article, a personalized dashboard, and a signed file download. Expected reasoning: use immutable versioned assets for long caching, validators and bounded freshness for articles, private or no-store for sensitive dashboards, and align signed authorization lifetime with edge behavior.
Exercise 5: Capacity during cache loss
A service handles 80,000 reads/s at 98% hits while the database safely handles 4,000 reads/s. Determine cache-loss behavior. Expected reasoning: normal source load is about 1,600 reads/s, but complete loss exposes 80,000 reads/s. Reserve source headroom, limit fills, prioritize critical reads, serve bounded stale data, and shed excess demand.
18. Interview questions and model answers
1. Why is cache invalidation considered difficult?
The authoritative update and removal of every distributed copy are usually separate operations. Processes can crash between them, messages can be lost or reordered, readers can repopulate old state, and different layers have different lifetimes. A strong answer names an allowed staleness bound and combines ordering, durable invalidation, versions, and TTL rather than claiming one delete is atomic with the database.
Follow-up: What if a price must never be stale at checkout?
Treat cached price as a browsing hint and validate current price and version inside the authoritative checkout transaction before commitment.
2. Compare cache-aside and read-through.
In cache-aside, application code performs get, source load, and put. It is explicit and flexible but easily duplicated. In read-through, a cache abstraction loads automatically, centralizing policy but hiding source behavior unless carefully designed. Both still need key correctness, coalescing, deadlines, error classification, and invalidation.
3. How do you choose a TTL?
Start from the maximum harmful staleness for the data class. Then consider update frequency, miss cost, invalidation reliability, source capacity, and outage policy. Validate with measured hit ratio and stale age. Add jitter. Different fields or endpoints may need different TTLs.
4. How do you prevent a cache stampede?
Combine staggered expiry, single-flight request coalescing, refresh-ahead or probabilistic early refresh for hot entries, safe stale serving, and hard source concurrency limits. Bound waiters by their deadlines and handle a failed refresh owner.
Follow-up: Why is a distributed lock not automatically the best answer?
It adds coordination latency, availability dependence, expiry races, and ownership problems. Per-node coalescing plus source limits may provide enough protection with fewer failure modes.
5. What is the difference between eviction and expiry?
Expiry removes or marks an entry unusable because its policy lifetime ended. Eviction removes an entry to reclaim capacity, potentially while it is fresh. A high eviction rate can reduce hit ratio even when TTLs are long.
6. Explain LRU versus LFU.
LRU favors recently used items and adapts quickly, but scans can displace the working set. LFU favors frequently used items and protects stable hot keys, but needs frequency decay when popularity changes. Real systems often approximate both for lower metadata cost.
7. What makes a cache key safe?
It uniquely describes the representation and its security boundary. Include stable tenant, object, schema, locale, content-negotiation, and policy dimensions that truly alter output. Canonicalize inputs consistently. Exclude secrets and noisy dimensions that do not affect output. Test collisions and cross-user behavior.
8. When should a service fail open or fail closed?
Base the decision on harm. Public content can often use bounded stale data. Authorization commonly fails closed because a stale allow is dangerous. Rate limiting needs a deliberate conservative degraded mode because pure fail-open permits abuse and pure fail-closed causes an outage.
9. Why can a cache outage cause a database outage?
The database is usually sized for misses, not total traffic. Removing a 98% hit cache can increase database demand 50 times. Unbounded fallback, synchronized retries, and cold fills then amplify queueing. Protect the source with limits, stale service, staged warm-up, and load shedding.
10. How does HTTP revalidation differ from fetching again?
A client sends a validator such as an ETag. If the representation is unchanged, the origin returns 304 without the full body. It still requires a request and origin decision, but saves transfer and possibly rendering work.
11. How would you cache multi-tenant data?
Use stable tenant identity in keys, authenticate and authorize at the correct boundary, separate public from private representations, apply tenant quotas, encrypt transport, redact diagnostics, and test that one tenant cannot infer existence, values, timing, or key structure of another.
12. What metrics prove a cache helps?
Hit and byte-hit ratios are only the start. Show reduced source requests, CPU, connections, bandwidth, and cost, plus better user latency. Also show staleness, invalidation lag, cache errors, evictions, hot-key skew, and behavior during cold start. A high hit ratio with wrong data is a failure.
13. When is write-behind appropriate?
When delayed persistence and reordering are acceptable or when the buffering layer is engineered as a durable write system. It needs durable replication, idempotent replay, backpressure, reconciliation, and explicit acknowledgement semantics. It is not appropriate merely because synchronous writes seem slow.
14. How do versioned keys help?
A new value receives a new identity, so old copies cannot masquerade as new. This allows long lifetimes for immutable content and reduces purge dependence. The system still needs a way to publish current references and garbage-collect old versions.
15. Design a cached content feed.
Clarify personalization, freshness, fan-out, privacy, and update rate. Cache public ranking candidates and per-user page IDs separately, fetch mutable item details by version, use short negative caching for removed items, coalesce misses, and keep authorization outside unsafe shared layers. Measure feed age and source offload. Use a safe older feed during a ranking outage.
19. Revision cheat sheet
- A cache is a limited, faster, disposable copy. The authority must be explicit.
- Value comes from locality, reuse, high miss cost, and a manageable working set.
- Measure request hits, byte hits, source offload, latency, cost, and correctness.
- Cache layers include client, process, distributed, database, proxy, edge, and CDN.
- Cache-aside is explicit; read-through centralizes loading; memoization keys function inputs.
- Write-through persists synchronously; write-behind changes the durability boundary.
- Commit authority first, invalidate after, repair durably, and bound with TTL.
- TTL is allowed staleness. Add jitter and use different policies by data class.
- Versioned keys avoid reuse across versions; tags enable grouped invalidation.
- Coalesce concurrent misses and limit source load to stop stampedes.
- LRU uses recency; LFU uses frequency; admission prevents low-value pollution.
- Negative-cache only authoritative absence and usually use a short TTL.
- Hot keys require replication, near-caches, coalescing, or data decomposition.
- HTTP
no-cachemeans validate;no-storemeans do not store. - A shared cache key is a security boundary. Test poisoning and tenant isolation.
- Choose fail-open, fail-closed, stale, or bypass behavior from business harm.
- Cold-start and purge capacity matter more than steady-state capacity.
20. Primary official references
- IETF RFC 9111: HTTP Caching - freshness, validation, cacheability, shared and private caches, and security considerations.
- IETF RFC 9110: HTTP Semantics - methods, validators, representations, conditional requests, and field semantics.
- RFC 5861: HTTP Cache-Control Extensions for Stale Content - stale-while-revalidate and stale-if-error.
- IETF RFC 9211: The Cache-Status HTTP Response Header Field - interoperable cache diagnostics and their security implications.
- Redis official key eviction documentation - bounded memory, approximate LRU, LFU, and eviction-policy behavior.
- Google Cloud CDN official caching overview - request collapsing, validators, cacheability, and edge behavior.
- Cloudflare official Tiered Cache documentation - hierarchical edge caching and origin-load reduction.
- OWASP Cache Poisoning guidance - shared-cache attack impact and defensive testing context.