Skip to content

Performance Engineering and System Design Handbook / Chapter 21

Caching as a Consistency and Capacity Design

Design caches as workload-dependent replicas with explicit avoided work, authority, freshness, invalidation, cold behavior, and failure transfer.

Mercury API’s release dashboard declared its cache rollout a success: request hit ratio rose from 82% to 96%, and median latency fell. Origin CPU fell by only about 13%. Fleet CPU, after cache lookup and invalidation work, fell by about 7%. Response-byte hit ratio was 43%. Request p99 rose because a miss first waited 12 ms for a cache timeout and then paid the origin’s 42 ms path.

None of those measurements contradicts another. The cache absorbed many cheap, small requests while the misses retained most expensive origin work and bytes. It made the common path faster, inserted another failure boundary, and left the constraining path nearly intact. “Ninety-six percent hit ratio” described lookup outcomes, not system value.

That result changes the design question. A cache is not a speed layer added between caller and service. It is a replica or retained computation with a key, value, authority relationship, reuse population, freshness contract, replacement policy, repair path, and failure mode. Add one only after identifying which work it avoids and which obligations its duplicate state creates.

A hit must name the work it avoided

Define a request population over one stated window and boundary. For class (i), let (\lambda_i) be requests/s, (h_i) its cache-hit fraction, (b_i) origin response bytes/request, and (d_i) origin service demand in resource-ms/request. Three ratios answer different questions:

[ H_{request} = \frac{\sum_i \lambda_i h_i}{\sum_i \lambda_i} ]

[ H_{byte} = \frac{\sum_i \lambda_i h_i b_i}{\sum_i \lambda_i b_i} ]

[ H_{work} = \frac{\sum_i \lambda_i h_i d_i}{\sum_i \lambda_i d_i} ]

Mercury’s modeled traffic has 8,640 small hot-description reads/s at 2,000 bytes and 0.05 CPU-ms of origin work, plus 360 uncached derived-view reads/s at 64,000 bytes and 8 CPU-ms. Caching every small read produces:

measure modeled result question it answers
request hit ratio 96.0% How many lookups avoided an origin request?
byte hit ratio 42.9% How many origin response bytes were avoided?
avoided origin CPU work 13.0% How much modeled origin CPU demand disappeared?
net fleet CPU saving 222 CPU-ms/s, or 6.7% of baseline origin demand What remains after 180 CPU-ms/s of lookups and 30 CPU-ms/s of invalidation?

The fixture at examples/performance-engineering-system-design-handbook/part-03/cache-consistency-capacity/ reproduces these values. They are modeled teaching inputs, not observations from a cache implementation.

A useful hit can avoid database reads, remote round trips, decompression, authorization joins, ranking, storage I/O, egress, or a scarce license/API call. Attach the hit to the resource and critical path it changes. If the saved CPU was never constraining but every miss still consumes the saturated storage pool, throughput may not move. If cache lookup allocates more memory, adds a network hop, or duplicates payloads, a local latency improvement can raise total cost.

Also count useful hits. A nominal hit followed by revalidation, missing-field fetch, or rejected stale version may avoid little work. A hit that returns an incorrect price, deleted object, or revoked permission is not goodput. Report hit outcomes by operation, tenant, object-size band, source version, and freshness class rather than as one fleet average.

Four-panel analytical cache design map: placement and authority across local, distributed, and origin layers; a reuse-distance capacity curve with diminishing returns after 50,000 entries; a cold-key stampede timeline collapsing 144 origin fills to one coalesced fill; and an economics ledger that separates request hits, byte hits, avoided backend work, cache cost, and failure transfer.
A cache earns its place through avoided backend work and controlled consistency, cold-start, and failure behavior—not through request hit ratio alone.

Placement decides latency, sharing, and failure scope

“The cache” can be several state copies with different populations. Identify each copy separately.

placement reuse population principal gain principal obligation
request-local memo one call graph remove duplicate computation within a request bound lifetime; do not reuse across changed inputs
process-local one process no remote hop; object-native values per-process duplication, uneven warmth, deployment churn
node-local processes on one host shared host reuse and larger pool local IPC, host failure, tenant isolation
distributed service fleet or cell shared capacity and coordinated keys network tail, hot shards, separate availability and overload
client one user, device, or SDK remove service and network work versioned invalidation, privacy, clock/offline behavior
edge geographic/client cohort distance and origin egress avoidance cache-key correctness, regional invalidation, sensitive data
storage/page/block storage engine and device path avoid parsing, I/O, and device access double caching, write coherence, memory-pressure interaction

Local caches usually minimize lookup latency but multiply memory by process count. A 2 GiB per-process cache on 24 processes does not create a 48 GiB shared working set; it creates 24 independently warmed populations, often containing the same hot values. A rolling deployment replaces those populations in waves and can synchronize misses.

A distributed cache can consolidate capacity and retain warmth across application deploys. It also adds serialization, network, queue, and remote-failure terms to every attempted lookup. A request that times out against that cache and then falls back has a serial critical path unless the design uses a tightly bounded alternative. Hedging cache and origin reads raises origin load and demands cancellation and result-version rules; it is not free tail insurance.

Client and edge caches have the longest invalidation path. They can deliver the largest distance and origin savings, but the service might not control when a disconnected client returns. Sensitive and user-specific responses require key partitioning, storage policy, deletion propagation, and shared-cache directives that match the authorization boundary. A missing Vary dimension or tenant identity can turn a performance feature into data exposure.

Multiple levels compose only when their keys, versions, and age semantics compose. A fresh process-local entry copied from an already stale distributed entry is not fresh relative to the authority. Carry source version or generation time; do not restart age at each hop.

Read and write paths choose who waits for coherence

The familiar strategy names encode different ownership of misses and updates:

strategy read miss write path favor when reject or guard when
cache-aside application reads origin, then fills update origin; invalidate or update cache separately application needs explicit control and misses are tolerable dual actions can race; fill must not overwrite a newer value
read-through cache layer loads origin separate write policy many callers need one loading contract loader failures and authorization cannot be hidden behind a generic interface
write-through read can use cache after synchronous cache+origin policy caller waits for both required effects cached read state must track acknowledged writes cache failure must not ambiguously change authoritative commit outcome
write-behind cache may accept/mutate before origin origin update is asynchronous coalescing is valuable and loss/reorder semantics are designed cache becomes durable write state; recovery and backpressure become mandatory
refresh-ahead refresh starts before expiry authority unchanged hot predictable keys justify proactive work refresh can waste capacity or amplify load during origin degradation

Cache-aside has a classic stale-fill race. Reader A misses and starts an origin read at version 41. Writer B commits version 42 and invalidates the key. Reader A then stores version 41 after the invalidation. A TTL eventually removes the error, but TTL did not provide coherence. Compare versions on fill, use generation/epoch keys, serialize update/fill for the key, or read from a snapshot whose version is recorded in the entry.

Write-through must state what the acknowledgement means. If the origin is authoritative, a cache write failure after an origin commit should not cause an unsafe retry that repeats the write. If both cache and origin are required before acknowledgement, partial outcomes need reconciliation. Calling the pair “atomic” without a transaction boundary is not a protocol.

Write-behind changes more than latency. The cache or its log now holds authoritative pending writes until the origin applies them. It needs durable admission, ordering scope, idempotent effects, retry bounds, dead-letter handling, lag objectives, and rebuild rules. Chapter 19’s asynchronous lifecycle applies; a volatile eviction policy cannot govern uncommitted business state.

Refresh-ahead should spend work only on keys likely to be reused. Refreshing every entry at 80% of TTL can generate a background scan whose rate is determined by cache population rather than current demand. Add jitter, admission by recent use, a refresh concurrency limit, and origin credits. Stop speculative refresh before it competes with demanded misses.

Capacity follows reuse distance, size, and miss cost

An entry is worth retaining when it is likely to be reused before eviction and its avoided value exceeds its residency and maintenance cost. Reuse distance is the number of distinct keys referenced between two references to the same key. A cache that can retain more than that distance can hit under a recency policy; a smaller cache cannot. Real policies also consider frequency, size, admission, and expiry.

Mercury’s deterministic reuse-distance fixture supplies this curve:

capacity modeled cumulative request hit ratio marginal gain from prior point
100 entries 58.0%
1,000 entries 82.0% 24.0 percentage points
10,000 entries 93.0% 11.0 points
50,000 entries 96.0% 3.0 points
100,000 entries 96.5% 0.5 points

The next 50,000 entries buy only half a percentage point in this modeled population. That can still be valuable if those entries avoid expensive work, but request ratio alone cannot decide it. Plot hit and avoided-work curves against both entry count and resident bytes. Repeat by tenant and phase because a global curve can conceal a large tenant evicting every small tenant’s working set.

Eviction chooses a victim among admitted entries; admission decides whether a miss should displace anything at all. Scans and one-hit objects can destroy a recency cache by filling it with data that will not be reused. Frequency-aware admission, a small probationary segment, or size/miss-cost weighting can protect the established working set. TinyLFU is one primary example of frequency-sketch admission; its result is a mechanism to evaluate, not a universal policy.

Variable sizes make entry count misleading. An object ten times larger should displace ten units of byte capacity, and its benefit should reflect both reuse and avoided miss cost. A practical score may estimate:

[ V_k = \frac{p_{reuse,k} \times C_{miss,k}}{B_k \times C_{residency,k}} ]

where (p_{reuse,k}) is estimated reuse probability within the relevant horizon, (C_{miss,k}) is avoided latency/resource/economic cost, (B_k) is resident bytes, and (C_{residency,k}) represents memory and maintenance cost. The formula is a ranking aid, not proof: estimates are noisy, and correctness or tenant reservations can override economic rank.

Expiration and eviction are distinct. TTL ends permission to reuse without validation under the application contract. Eviction frees capacity even when an entry remains semantically fresh. A system with constant eviction churn might never approach its designed TTL; a system with ample memory might retain many expired entries until lazy cleanup, affecting memory even though they cannot serve hits.

Freshness is a user-visible, field-level contract

“Eventually consistent cache” is not a freshness specification. Define:

  • the authoritative source and version domain;
  • the maximum staleness by operation and field;
  • whether the bound is time, source versions, events, or business state;
  • how a reader learns age/version and what it may do when uncertain;
  • which update, delete, revocation, or correction forces invalidation;
  • what happens when invalidation or validation is unavailable; and
  • how the system detects and repairs divergence.

A single composite response can contain different contracts. Mercury’s product description and images may tolerate 60 s of age. Price and availability for checkout must reflect at least the source version attached to the cart calculation. Authorization may require validation on every relevant version change. Caching the whole response under one 60 s TTL silently gives strict fields the weakest contract.

Split values by authority and freshness class, or cache a composite with field versions and revalidate strict fields before the consequential action. The browse page can display a descriptive snapshot and a clearly versioned availability hint; checkout must consult the authoritative inventory/price boundary. This preserves a fast browse path without claiming that browse-cache freshness authorizes a sale.

TTLs bound age only under their clock and fill assumptions. They do not guarantee global invalidation, prevent stale-fill races, or ensure a client refreshes its display. RFC 9111 defines HTTP freshness, validation, invalidation behavior on traversed caches, and controls such as must-revalidate; it explicitly does not make every appropriate cached representation globally disappear after an update.

Invalidation mechanisms include key delete, versioned key, generation/epoch bump, dependency tags, change-event consumption, conditional validation, and short TTL. Each moves cost differently. Key deletion is direct but must reach every copy. Versioned keys prevent old fills from replacing new state but leave garbage to reclaim. Dependency tags can invalidate a materialized view fan-out but require a bounded reverse index. Change streams scale propagation but inherit queue lag and replay semantics. Conditional validation spends origin work to prove reuse.

Negative caching retains “not found,” rejection, or failure outcomes. It is valuable for repeated nonexistent keys and hostile scans, but its TTL and invalidation must match creation semantics. A long negative entry can hide a newly created object. Do not cache authentication failures across principals, or transient overload as if it were permanent absence. Bound key cardinality so attackers cannot turn negative entries into memory exhaustion.

Deletes, privacy requests, and revocations must enumerate derived copies. “The database row was deleted” is incomplete if process caches, distributed caches, edge nodes, client storage, backups, and logs retain it. Define which copies are immediately invalidated, cryptographically made inaccessible, aged out under a legal policy, or handled by a separate retention process.

Cold behavior is part of normal operation

Every cache becomes cold after first deployment, fleet scale-out, process restart, shard movement, eviction storm, regional failover, key-version change, or a workload phase shift. Warm steady-state capacity is not deployment capacity.

Prewarming can replay a known hot set, transfer entries from an old generation, or issue synthetic reads. It consumes origin, network, cache write, and validation capacity at exactly the moment a rollout may already duplicate fleet work. Bound prewarm rate, prioritize by expected avoided cost, and prove the entries are still valid. Never copy a value into a new generation without its authority version and remaining age.

A rolling deployment creates mixed warm and cold instances. Random load balancing can send the same hot key to every new local cache, multiplying fills by the number of processes. Prefer a small shared lower-level cache, stable affinity where safe, or rollout gates based on origin headroom and cache readiness. Warmth is not a health check if correctness validation has not completed.

Measure cache state explicitly during change: eligible hit ratio, miss rate, fill rate, rejected refreshes, origin service demand, key and byte churn, eviction reason, age/version distribution, and rollout cohort. A deployment that holds global hit ratio steady can still overload one origin partition because new instances share the same hot misses.

One miss must not become 144 origin fills

At 3,600 requests/s for one cold key and a 40 ms origin fill, the modeled number of requests arriving before the first fill completes is:

[ N = 3600\ \text{requests/s} \times 0.040\ \text{s} = 144\ \text{requests}. ]

Without coordination, one expiration can produce 144 concurrent origin reads. If the fill slows under that load, its window grows and admits still more duplicates: a stampede feedback loop.

time        t0                 t0 + 40 ms                  after fill
key state   expires            origin read in flight      version 73 cached
requests    1, 2, 3 ... 144    duplicates accumulate      ordinary hits
unsafe      144 origin fills -> origin slows -> fill window expands
coalesced   one leader fill; bounded waiters share result; excess is rejected/stale

Request coalescing elects one in-flight fill per key and lets a bounded waiter set share its result. It needs a maximum wait, cancellation rules, result-version validation, and a failure policy. An unbounded waiter list merely moves the queue into memory. If the leader fails, do not release every waiter to retry simultaneously; transfer leadership with jitter or return a controlled outcome.

Other controls solve different cases:

  • stale-while-revalidate permits a declared stale window while one refresh runs; it trades freshness for latency and origin protection;
  • stale-if-error permits stale serving for named failures and content classes, never as an accidental fallback;
  • probabilistic early refresh spreads refreshes before common expiry, but still needs origin bounds;
  • TTL jitter desynchronizes independent keys but does not solve one hot key;
  • leases or fill tokens bound who may publish a fill, but lease expiry and fencing must prevent an old slow filler overwriting a new value;
  • negative caching absorbs repeated absence, with creation-aware invalidation; and
  • admission/load shedding rejects low-value misses before they consume a failing origin.

RFC 5861’s HTTP stale-while-revalidate and stale-if-error controls are useful protocol examples. They authorize stale reuse only within declared windows; they do not decide whether stale price, permission, or safety data is acceptable. Application semantics must do that.

Hot keys are capacity and placement problems

A key can be hot in requests, bytes, fill cost, write invalidations, or fan-out. Replicating a read-hot immutable value across cache shards spreads lookup work. Replicating a frequently updated value multiplies invalidation and coherence work. Sharding a single key by a suffix spreads reads only if every shard receives compatible versions and the caller can select one without rebuilding a new bottleneck.

Detect hotness at the key or bounded heavy-hitter level, not by shard average. Useful signals include requests/s, bytes/s, miss/fill rate, coalesced waiters, origin demand, invalidations/s, value size, and version lag. High-cardinality telemetry itself needs bounded sketches or sampled exemplars.

Fan-out makes one update touch many cached views. A product price change may invalidate product, search result, recommendation, category, cart-preview, and edge entries. Immediate eager invalidation reduces stale exposure but creates an update burst. Lazy version validation defers work to reads but can concentrate it on the next hot access. Materialize fewer composites, separate strict fields, or use generation tags with bounded cleanup.

Hot-key containment should preserve tenant fairness. A global hot object can consume fill concurrency and cache bandwidth while other tenants’ ordinary misses time out. Reserve origin/fill capacity by class, cap per-key and per-tenant waiters, and charge refresh work to a budget. Replicas are not isolation if they still share one saturated origin partition or invalidation stream.

Cache failure transfers load; it does not remove demand

Mercury’s origin sustains 1,800 requests/s. A total cache bypass transfers 9,000 requests/s, five times that capacity. “Fail open to origin” is therefore a collapse policy unless admission already limits the transferred work.

cache state safe behavior depends on common unsafe default
healthy/warm verified freshness and measured avoided work optimizing aggregate hit ratio only
cold/recovering origin headroom, prioritized prewarm, bounded fills allowing every miss and refresh
slow/partial cache timeout budget, per-key coalescing, origin admission serially waiting then falling back without capacity check
unavailable stale contract, criticality, origin capacity universal bypass
divergent version evidence and repair path serving “fresh by TTL” despite source mismatch

Choose among bypass, fail closed, stale serve, partial response, and rejection by operation. Public descriptive content can often use bounded stale data. A checkout price may require authoritative validation or a clear inability-to-complete outcome. Authorization should fail closed when stale state could grant access, though a separately designed capability may support a bounded offline case. The policy is an application decision, not a cache-client default.

Circuit breakers around a cache can create synchronized bypass. A fleet-wide breaker opening at once sends a step load to the origin. Use cell-scoped state, admission before fallback, jittered probes, reserved origin capacity, and a degradation ladder. Recovery also needs control: repopulating every key while serving misses can keep the origin saturated after the cache returns.

Observe the transferred path before an incident. Load-test total and partial cache loss with representative hot keys, object sizes, tenant mix, and update rate. Verify correctness while stale, version-uncertain, and repairing. Measure protected goodput and origin queue age, not how many calls the API layer accepted.

The economics worksheet prevents hit-ratio theater

Use the same window and workload for baseline and treatment:

ledger line unit baseline cached design evidence required
offered requests requests/s by class 9,000 9,000 validated generator or production count
successful useful completions goodput/s record record correctness and freshness checks
origin work CPU-ms/s, reads/s, bytes/s 3,312 CPU-ms/s 2,880 CPU-ms/s modeled per-operation service demand
cache lookup CPU-ms/s, calls/s, bytes/s 0 180 CPU-ms/s modeled hit and miss path profiles
invalidation/refresh/fill work/s 0 30 CPU-ms/s invalidation plus measured fill update and cold-state evidence
user latency p50/p95/p99 by result class record record hit/miss/stale/rejected separately end-to-end histogram, not averaged percentiles
resident state bytes by tier and tenant 0 measure entry-size and allocator/accounting evidence
avoided external cost reads, egress, paid calls 0 model and observe same unit of useful work
failure transfer origin requests/s 9,000 baseline design path up to 9,000 on cache loss cache-loss experiment and admission result
recovery time, peak origin work, stale exposure n/a measure cold/rebuild test

Calculate the net value over a relevant horizon:

[ V_{net} = V_{avoided\ origin} - C_{lookup} - C_{state} - C_{fill} - C_{invalidation} - C_{operations} - C_{failure\ risk}. ]

Do not force every term into speculative currency. CPU-seconds, storage reads, GiB transferred, engineer-hours, and risk bounds can remain separate until a decision requires conversion. Show uncertainty. A cache can be justified for latency even when infrastructure cost rises; say which objective pays for it.

To decide whether the cache improved capacity or hid an undersized backend, test the origin directly. At equal offered workload, compare origin service demand per useful completion, saturation knee, queue age, and goodput with cache hits, forced representative misses, and cache loss. If the origin still cannot carry the admitted miss envelope or deployment/recovery load, the cache is a dependency on warm state rather than sufficient capacity. That may be acceptable only with explicit availability, admission, and recovery contracts.

Applied design: hot reads with strict fields

Mercury serves 9,000 product-view requests/s; one product reaches 3,600 requests/s during launches. Descriptions and images may be 60 s old. Displayed availability can be a versioned hint, but checkout price and inventory must be validated against the source version used for reservation.

A defensible design separates the response:

  1. Cache descriptive content at the edge and distributed tier under a product-content version, with a 60 s freshness lifetime, bounded stale-if-error policy, and event-driven invalidation. Client responses expose content age/version.
  2. Cache availability hints for browsing under a short source-version contract, but label them non-authoritative. Checkout sends the observed version to the reservation authority, which accepts, returns a newer offer, or rejects; it never treats the browse hit as inventory ownership.
  3. Keep per-process request memoization for duplicate reads inside one call, but do not create an independent long-lived price cache.
  4. Coalesce one fill per (product, content-version, locale) key. Cap waiters, give fills origin credits, and allow bounded stale descriptive content while refreshing.
  5. Replicate the launch key across read shards only after its update rate and invalidation fan-out fit. Carry versions so an old filler cannot overwrite a newer value.
  6. Prewarm only the scheduled launch set, rate-limited below reserved origin headroom. A rollout cohort advances when origin queue age, version correctness, and useful-hit evidence remain healthy.
  7. On distributed-cache loss, serve eligible stale descriptions, validate checkout fields at the authority, reject low-value uncached enrichment, and admit origin product reads below 1,800 requests/s. Do not bypass all 9,000 requests/s.

The design earns its cache through avoided work and controlled degradation, not the 96% target. It also exposes a likely architectural correction: expensive derived views remain the dominant origin work, so optimize, materialize, bound, or separately cache those only after measuring their reuse and correctness needs.

Operational proof across warm, cold, and failed states

Instrument a cache attempt with cache tier, key class, source version, entry age, outcome (hit, miss, stale, revalidated, rejected), fill leader/waiter, eviction/admission reason, and resulting origin call. Keep raw keys and user data out of telemetry; use bounded classifications and sampled protected identifiers where investigation requires them.

The decisive views are:

  • goodput, latency, correctness, and freshness by cache outcome;
  • request, byte, and avoided-work hit ratios by operation and tenant;
  • cache lookup/fill/invalidation/refresh service demand;
  • origin demand, queue age, saturation, and error/rejection rate;
  • reuse-distance or stack-distance curve, entry-size distribution, eviction churn, and admission decisions;
  • source-to-entry version lag and invalidation propagation delay;
  • per-key fill concurrency, coalesced waiter count, and leader failures;
  • warm-up progress and origin headroom by deployment cohort; and
  • cache-loss transfer, stale exposure, protected goodput, and recovery duration.

Run four experiments at equal offered load: warm steady state, cold start, one-tier partial failure, and complete distributed-cache loss. Add a hot-key expiration, invalidation lag, stale-fill race, and origin slowdown. Assert user-visible semantics, not merely absence of errors. A cache benchmark with uniform random keys, fixed objects, no updates, and a warm preloaded population cannot validate this design.

Roll out by cell or tenant, preserve a no-cache control population where safe, and compare origin work per useful completion. A cache-key or serialization change creates a cold generation; treat it as a migration with rate limits and rollback. Keep an immediate way to disable unsafe hits without opening an unlimited origin bypass.

Choose the copy and the coherence path together

Start with the reuse population. A process-local cache earns its very low lookup cost when reuse is local and values are small enough that duplication and rolling coldness remain cheap. Per-process resident bytes, warmth, and source-version lag decide whether that remains true. When the same working set is reused across the fleet, a distributed cache can retain shared warmth and capacity—but only if the avoided work pays for serialization, the network tail, hot shards, and a separate failure domain. End-to-end work, shard heat, and a cache-loss experiment decide between those placements more honestly than lookup latency alone.

Then choose who owns a miss. Cache-aside keeps that control in the application and is often the narrowest workable mechanism, provided a versioned race test shows that an old fill cannot follow a newer invalidation. Read-through centralizes loading for many callers, but the common interface must still expose authorization, loader failure, and result-version semantics. Either approach fails when it turns a miss into an unbounded second request path.

The write acknowledgement sets a harder boundary. Write-through fits when an acknowledged update must also be visible through the cached read path and mixed cache/origin failures have a defined outcome. Test partial failure and reconciliation rather than assuming the two effects are atomic. Write-behind is justified only when coalescing or shorter foreground service is worth making the cache or its log authoritative pending state. If lag, loss, ordering, replay, and duplicate effects cannot be bounded, it is not a cache optimization the system can safely claim.

Refresh-ahead spends origin work before a demanded miss. Use it only for predictably reused hot keys and only while refresh-utility measurements and origin credits show that speculation is paying for itself. A population scan that continues during origin degradation reverses the intended protection. Bounded stale serving makes a different trade: it can preserve availability and origin headroom for content whose semantics tolerate a named age or error window, but it must remain unavailable to safety, authorization, or transactional fields whose invariants stale data can violate. Stale-window correctness tests, with age and version visible, are the deciding evidence.

The rule is strict because these choices interact: add a cache only when the avoided work, staleness contract, invalidation mechanism, cold behavior, and failure transfer are understood.

Field checklist

  • Which source is authoritative, and which cache copies exist at process, node, service, client, edge, and storage layers?
  • What request, byte, resource-work, and useful-hit ratios matter by operation and tenant?
  • Which exact work does each hit avoid, and was that work constraining?
  • What is the freshness/version contract for every consequential field and negative result?
  • How are stale fills, concurrent writes, deletes, revocations, and privacy obligations fenced and repaired?
  • What do reuse-distance, entry-size, miss-cost, and tenant curves say about capacity and admission?
  • How many origin fills can one hot miss create, and how are leaders, waiters, retries, and failures bounded?
  • What happens during cold start, scale-out, rolling deployment, failover, and cache-key migration?
  • Can the origin carry admitted miss, bypass, and recovery load without positive feedback?
  • Which operations serve stale, bypass, degrade, reject, or fail closed when a cache is slow, unavailable, or divergent?
  • Does the economics ledger include lookup, state, fill, invalidation, operations, and recovery costs?
  • Which experiment proves end-to-end goodput, correctness, and constraint relief rather than warm hit ratio?

Design drill: make field freshness enforceable

For Mercury’s 9,000 requests/s workload, produce a design with:

  1. authority and cache placement for description, image, price, availability hint, reservation inventory, and authorization;
  2. keys and source versions for locale, tenant, and product generation;
  3. read, write, invalidation, stale-fill, delete, and repair sequences;
  4. a capacity/admission policy using reuse, bytes, miss cost, and tenant bounds;
  5. a stampede policy for the 3,600 requests/s launch key;
  6. a rolling-deployment and prewarm budget;
  7. healthy, cold, slow, unavailable, divergent, and recovering behavior;
  8. request/byte/work/goodput/freshness evidence; and
  9. rollback and revisit triggers.

One valid answer may cache only descriptive blocks and treat strict fields as authoritative calls. Another may cache versioned hints and require conditional reservation. Reject any answer that lets a 60 s composite TTL authorize checkout or that sends the full 9,000 requests/s to an origin sized for 1,800.

Diagnostic drill: did the cache remove the constraint?

You receive these facts after rollout: request hit ratio 96%; byte hit ratio 43%; origin request rate down 96%; origin CPU down 13%; fleet CPU down 7%; p50 down 18 ms; p99 up 12 ms; cache miss path has a 12 ms timeout; complete cache bypass offers five times origin capacity; the final 50,000 entries add 0.5 percentage points of request hits.

Rank at least three hypotheses and choose discriminating evidence. A strong analysis separates cheap hot hits from expensive misses, finds which origin resource still constrains, measures cache timeout contribution, tests object-size and tenant skew, values the last capacity increment, and blocks uncontrolled bypass. “Increase TTL” is not a diagnosis because it changes freshness and warm ratio without proving which work matters.

Review questions and durable conclusions

  1. Why can request hit ratio be 96% while avoided origin CPU is 13%? Write the weighted-work equation.
  2. A 3,600 requests/s key takes 40 ms to fill. How many duplicate fills can arrive, and which bound is still required after request coalescing?
  3. Explain the stale-fill race in cache-aside and give two version-aware defenses.
  4. Why does doubling cache entries from 50,000 to 100,000 need a byte/miss-cost analysis even when hit ratio rises?
  5. Which fields in a product response can share a TTL, and which require version validation at a consequential action?
  6. A cache breaker opens fleet-wide. Why can fallback be a positive-feedback loop, and where should admission occur?
  7. When does write-behind make the cache part of authoritative durable state?
  8. What evidence distinguishes a capacity improvement from warm-state dependence on an undersized origin?

The durable model is replicated state plus avoided work. Placement chooses who shares the replica and which failure domain it creates. Strategy assigns miss and update waiting. Capacity follows reuse distribution, bytes, miss cost, and admission—not configured memory alone. Freshness is a field- and action-specific correctness contract; TTL is only one enforcement input. Cold start, hot keys, stampedes, total loss, and recovery are ordinary design states. Cache value exists only when end-to-end goodput, constraint relief, and economics improve under those states.

Evidence and transfer limits

  • RFC 9111: HTTP Caching defines HTTP cache storage, freshness, validation, invalidation, stale reuse, and security considerations. It is a protocol contract, not a complete application-consistency design.
  • RFC 5861: HTTP stale controls defines stale-while-revalidate and stale-if-error as scoped response controls. Application semantics still decide whether a stale value is safe.
  • TinyLFU: A Highly Efficient Cache Admission Policy is a primary research source for frequency-sketch admission under skew. Its workload results do not select Mercury’s policy without representative validation.
  • Every Mercury number and curve in this chapter is deterministic modeled evidence from examples/performance-engineering-system-design-handbook/part-03/cache-consistency-capacity/. The fixture assumes two request classes, fixed service demands and payloads, deterministic fill time, and supplied reuse-distance points. It is not a production trace, implementation benchmark, or capacity guarantee.

Caching decides whether future work can reuse a state copy. The copy’s encoded shape determines its bytes, CPU, allocation, compatibility, validation, and transfer cost. That representation boundary is next.