An LRU thumbnail cache is bounded by entry count while items range from 2 KB to 2 MB — what breaks?
answer
- The count bounds the wrong quantity
- Ask what a full cache actually weighs
- Failure correlates with the traffic mix
- Eviction becomes a loop, not a step
- One item bigger than the whole budget
basics
~20 sA count bound only bounds memory when entries are uniform. With sizes spanning three orders of magnitude, the same entry count can mean fifty megabytes or fifty gigabytes, so the footprint is set by traffic mix, not by configuration.
solid answer
~50 sCounting entries is a proxy for memory, and the proxy fails when entry sizes vary by 1000x: a burst of large panorama variants keeps the entry count at its limit while resident bytes explode, and the process hits memory pressure at exactly the moment traffic is heaviest. The fix is to make the budget the thing you actually care about — bytes. Store each entry's size on its node, keep a running total, and on insert add the new size and then evict from the stale end in a `while total > budget` loop. That stays amortized O(1) per write because every entry is unlinked at most once in its lifetime. Two guards matter: reject or bypass an item larger than the whole budget rather than letting it flush the cache, and cap entry count as well so per-node overhead on millions of tiny entries stays bounded.
go deeper
Understand that a cache limit counted in entries says nothing about memory unless the entries are roughly the same size, and that thumbnails are not.
Explain the mechanics of a byte budget: size stored on the node, a running total, and eviction as a loop that runs until the total is back under the limit.
Demonstrate the operational reasoning — that the failure correlates with traffic mix, that the write bound is amortized rather than worst-case, and that an oversized item must be refused rather than admitted.
Own the resource-budget decision itself: which quantity the service can genuinely afford to bound, who is accountable when the bound is wrong, and whether a tail-latency budget makes amortized eviction unacceptable.
## Why a count bound is a bad proxy A capacity expressed as "5,000 entries" is really a bet that entries are interchangeable in size. When they are — fixed-width records, small tokens — the bet is fine and counting is cheaper than measuring. When they are not, the bound controls the wrong variable. A thumbnail cache is the canonical case. A tiny avatar variant might be 2 KB; a full-width hero rendering of a panorama might be 2 MB. A 5,000-entry cache is therefore anywhere between roughly 10 MB and 10 GB of resident data, and *which* it is depends on the mix of requests arriving right now. That is the dangerous property: the failure correlates with load. A promotion drives traffic toward large renderings, the cache fills with them at exactly the same entry count it has always held, and the process is pushed into memory pressure precisely when it can least afford it. Nothing in the cache's configuration changed; nothing in the metrics that watch entry count moved. ## Budgeting the resource you actually own Make the capacity a byte budget: 1. Store the entry's size on its node when it is admitted. Never recompute it at eviction time — the payload may be expensive to measure and the node must be able to account for itself when it is dropped. 2. Keep a running total of admitted bytes. 3. On admission, add the new entry's size to the total, then evict from the stale end **while** the total exceeds the budget, subtracting each victim's stored size. 4. On overwrite, subtract the old size and add the new one before running the same loop — an overwrite that grows an entry can push the cache over budget just as an insert can. The eviction loop is the part interviewers probe. A single write can now evict twenty entries, so the *worst-case* cost of one write is no longer constant. The bound that survives is **amortized**: every entry is linked exactly once when admitted and unlinked exactly once when evicted, so across any sequence of n writes the total eviction work is O(n) and the per-write average is constant. That is the same shape of argument as growth-doubling in dynamic arrays, and it carries the same caveat — amortized is a statement about a worst-case *sequence*, not a promise about any individual write. If your service has a tail-latency budget that a twenty-eviction write would blow, the amortized bound is not the answer; you would cap evictions per write and let the cache run transiently over budget, or shed the work to a background trim. ## The oversized-entry guard What happens when a single item is larger than the entire budget? The naive loop admits it, then evicts everything else, then evicts the new item itself, then finds the cache empty and still over budget — and either loops forever against the sentinels or leaves the cache empty after having thrown away every useful entry to make room for something it could not keep. This is a real outage shape, not a theoretical one: one pathological upload flushes a warm cache. The guard is admission control at its simplest: if an item's size exceeds the budget (or some fraction of it, commonly a small percentage, so that no single entry can dominate), do not admit it. Serve it and move on. The cache's job is to hold the many small things well, not to make room for the one thing that cannot fit. A second guard is worth stating: keep a *count* bound too. Byte budgeting alone lets a flood of 200-byte entries accumulate millions of nodes, and each node carries a payload-independent cost — two pointers, the key, the stored size, the map's own per-entry structure. A cache bounded only by payload bytes systematically undercounts its true footprint, sometimes by a large factor for tiny entries. Bounding both count and bytes keeps both failure modes closed. ## What to measure afterwards Entry hit rate stops being the interesting number once entries are heterogeneous. A cache that serves 90% of *requests* from memory but misses on the largest ones may be doing far worse for the resource that hurts — origin bandwidth, rendering CPU, tail latency. Measure hit rate weighted by the cost you are trying to avoid, and track the byte-size distribution of both hits and admissions. The pattern to watch for is a small number of large admissions repeatedly evicting a large number of small hot entries: the byte budget is being spent on entries with poor reuse, and the answer is a size-aware admission rule rather than a bigger budget. ## What does not change The structure itself is untouched. The map still maps keys to nodes, the doubly linked list still orders nodes by recency, reads still relink, and the victim is still the node before the tail sentinel. Weighted capacity changes only the *stopping condition* of eviction, from "one entry over" to "still over budget" — which is exactly the argument for why this composition is worth understanding as a pattern rather than memorising as a single canned design.
- If one write can evict twenty entries, is the write still O(1)?Amortized, yes; worst-case, no. Each entry is unlinked at most once in its lifetime, so eviction work across a sequence of n writes totals O(n) and averages to constant per write. But an individual write can be long, and amortized bounds say nothing about individual operations — if you have a hard tail-latency budget you must cap evictions per write and trim the excess elsewhere.
- How should the cache treat an item larger than its entire budget?Refuse to admit it. Admitting it evicts every other entry and then evicts the item itself, so the cache pays a full flush for zero benefit — one pathological item cold-starts the whole cache. A common rule is to reject anything above a small fraction of the budget, so no single entry can dominate the space even when it does fit.
- Why keep an entry-count bound alongside the byte budget?Because per-entry overhead is invisible to a payload-byte total. Every entry costs a node, two pointers, the key, the stored size and a map slot regardless of payload, so millions of tiny entries can occupy several times the bytes the budget thinks it is tracking. Bounding both closes the tiny-entry failure mode as well as the large-entry one.
A shelf rated for twenty books holds twenty paperbacks or twenty atlases; only one of those numbers tells you whether the shelf collapses.
saying these in an interview costs you the question
- Assumes entry count bounds memory regardless of sizes
- Recomputes an entry's size at eviction time
- Admits an item larger than the whole budget
- Calls a multi-eviction write worst-case constant time
- Reports entry hit rate for wildly heterogeneous entries