The governing arithmetic: two inequalities
When does a cache pay for itself, and how far does the layer below come to depend on it?
Two short inequalities run this mechanism. Every topic later is a resolution of one of them.
Amortization: does the cache pay?
Use these symbols:
- P = the cost to produce one version of the cached object (for example, to run a query).
- S = the saving for each read that is served from the cache.
- λ (lambda) = the rate of reads on the object.
- w = the rate of writes that invalidate it (make it wrong) — the churn rate.
One cached version lives for 1/w time units on average, and during that time it serves λ/w reads. Call R = λ/w the reuse factor (reads per version). The cache pays if and only if
- the rate of reads on the object
- the rate of writes that invalidate it; summed over its dependencies
- the cost of computing one version
- what serving one read from the cache saves
- reads per version
Data table
| Inputs the result depends on | Reuse factor R |
|---|---|
| 1 | 20 |
| 1.24 | 16.2 |
| 1.48 | 13.6 |
| 1.71 | 11.7 |
| 1.95 | 10.3 |
| 2.19 | 9.14 |
| 2.42 | 8.25 |
| 2.66 | 7.51 |
| 2.9 | 6.9 |
| 3.14 | 6.37 |
| 3.38 | 5.93 |
| 3.61 | 5.54 |
| 3.85 | 5.19 |
| 4.09 | 4.89 |
| 4.33 | 4.62 |
| 4.56 | 4.38 |
| 4.8 | 4.17 |
| 5.04 | 3.97 |
| 5.28 | 3.79 |
| 5.51 | 3.63 |
| 5.75 | 3.48 |
| 5.99 | 3.34 |
| 6.22 | 3.21 |
| 6.46 | 3.09 |
| 6.7 | 2.99 |
| 6.94 | 2.88 |
| 7.17 | 2.79 |
| 7.41 | 2.7 |
| 7.65 | 2.61 |
| 7.89 | 2.54 |
| 8.13 | 2.46 |
| 8.36 | 2.39 |
| 8.6 | 2.33 |
| 8.84 | 2.26 |
| 9.07 | 2.2 |
| 9.31 | 2.15 |
| 9.55 | 2.09 |
| 9.79 | 2.04 |
| 10 | 2 |
| 10.3 | 1.95 |
| 10.5 | 1.9 |
| 10.7 | 1.86 |
| 11 | 1.82 |
| 11.2 | 1.78 |
| 11.4 | 1.75 |
| 11.7 | 1.71 |
| 11.9 | 1.68 |
| 12.2 | 1.64 |
| 12.4 | 1.61 |
| 12.6 | 1.58 |
| 12.9 | 1.55 |
| 13.1 | 1.53 |
| 13.3 | 1.5 |
| 13.6 | 1.47 |
| 13.8 | 1.45 |
| 14.1 | 1.42 |
| 14.3 | 1.4 |
| 14.5 | 1.38 |
| 14.8 | 1.35 |
| 15 | 1.33 |
| 15.3 | 1.31 |
| 15.5 | 1.29 |
| 15.7 | 1.27 |
| 16 | 1.25 |
| 16.2 | 1.23 |
| 16.4 | 1.22 |
| 16.7 | 1.2 |
| 16.9 | 1.18 |
| 17.1 | 1.17 |
| 17.4 | 1.15 |
| 17.6 | 1.13 |
| 17.9 | 1.12 |
| 18.1 | 1.1 |
| 18.3 | 1.09 |
| 18.6 | 1.08 |
| 18.8 | 1.06 |
| 19.1 | 1.05 |
| 19.3 | 1.04 |
| 19.5 | 1.02 |
| 19.8 | 1.01 |
| 20 | 1 |
The value of one version is (λ/w)·S − P, and it must be positive.
The ratio P/S is a threshold: the number of reads you need before the cache is worth building. The inequality contains a lesson that practitioners often get wrong:
A million reads per second against a table written a million times per second cannot be cached usefully. A hundred reads an hour against an immutable file can. Caching converts a read-rate problem into a write-rate problem.
Leverage: how much do you depend on the cache?
If the cache sustains hit rate h, the layer below is asked for only (1 − h)λ requests per second. Sooner or later someone will size that lower layer for exactly that smaller load. Then the cache has leverage
- the fraction of reads the cache serves
- reads arriving at the cache
Data table
| Hit rate h | Leverage 1/(1 − h) |
|---|---|
| 0 | 1× |
| 0.012 | 1.01× |
| 0.025 | 1.03× |
| 0.037 | 1.04× |
| 0.05 | 1.05× |
| 0.062 | 1.07× |
| 0.075 | 1.08× |
| 0.087 | 1.1× |
| 0.1 | 1.11× |
| 0.112 | 1.13× |
| 0.124 | 1.14× |
| 0.137 | 1.16× |
| 0.149 | 1.18× |
| 0.162 | 1.19× |
| 0.174 | 1.21× |
| 0.187 | 1.23× |
| 0.199 | 1.25× |
| 0.211 | 1.27× |
| 0.224 | 1.29× |
| 0.236 | 1.31× |
| 0.249 | 1.33× |
| 0.261 | 1.35× |
| 0.274 | 1.38× |
| 0.286 | 1.4× |
| 0.298 | 1.43× |
| 0.311 | 1.45× |
| 0.323 | 1.48× |
| 0.336 | 1.51× |
| 0.348 | 1.53× |
| 0.361 | 1.56× |
| 0.373 | 1.6× |
| 0.386 | 1.63× |
| 0.398 | 1.66× |
| 0.41 | 1.7× |
| 0.423 | 1.73× |
| 0.435 | 1.77× |
| 0.448 | 1.81× |
| 0.46 | 1.85× |
| 0.473 | 1.9× |
| 0.485 | 1.94× |
| 0.497 | 1.99× |
| 0.51 | 2.04× |
| 0.522 | 2.09× |
| 0.535 | 2.15× |
| 0.547 | 2.21× |
| 0.56 | 2.27× |
| 0.572 | 2.34× |
| 0.585 | 2.41× |
| 0.597 | 2.48× |
| 0.609 | 2.56× |
| 0.622 | 2.64× |
| 0.634 | 2.73× |
| 0.647 | 2.83× |
| 0.659 | 2.93× |
| 0.672 | 3.05× |
| 0.684 | 3.17× |
| 0.697 | 3.29× |
| 0.709 | 3.44× |
| 0.721 | 3.59× |
| 0.734 | 3.76× |
| 0.746 | 3.94× |
| 0.759 | 4.14× |
| 0.771 | 4.37× |
| 0.784 | 4.62× |
| 0.796 | 4.9× |
| 0.808 | 5.22× |
| 0.821 | 5.58× |
| 0.833 | 6× |
| 0.846 | 6.48× |
| 0.858 | 7.05× |
| 0.871 | 7.73× |
| 0.883 | 8.55× |
| 0.895 | 9.57× |
| 0.908 | 10.9× |
| 0.92 | 12.6× |
| 0.933 | 14.9× |
| 0.945 | 18.3× |
| 0.958 | 23.6× |
| 0.97 | 33.5× |
| 0.983 | 57.3× |
| 0.995 | 200× |
against the backing store. At h = 95% the leverage is 1/0.05 = 20 to 1. At h = 99% it is 1/0.01 = 100 to 1. And look at what happens at the top: a drop from h = 0.99 to h = 0.98 changes the load below from 0.01λ to 0.02λ. A one-point drop in hit rate at 99% doubles the load on the layer below.
Hit rate is leverage, and leverage is fragility. This is how an optimization quietly turns into a load-bearing dependency that appears on no dependency diagram. It is exactly what happened to Ifeoma's database when the cache was restarted.