Why Large Objects Miss More Often at the Edge
Problem Statement
Site-wide hit ratio reads 95%, and the origin’s egress bill keeps rising. Splitting the metric by response size explains it: small assets hit almost every time, and the handful of objects that account for most of the bytes miss almost every time. Three separate mechanisms produce that shape, and they need different fixes.
Prerequisite Concepts
- Cache storage, eviction and capacity — the capacity model that underlies two of the three mechanisms.
- Cache hit, miss and bypass mechanics — the outcomes being measured.
- Range requests and partial content caching — why media is stored in blocks rather than whole objects.
Step-by-Step Resolution
Step 1 — Split hit ratio by response size
A single site-wide number hides the problem completely, because it is dominated by whichever objects are most numerous:
awk '{
size = $10; status = $NF; # adjust field positions to your log format
b = size < 102400 ? "small" : (size < 10485760 ? "medium" : "large");
total[b]++; if (status ~ /HIT/) hit[b]++
} END {
for (b in total) printf "%-7s %6.1f%% hit (%d requests)\n", b, 100*hit[b]/total[b], total[b]
}' /var/log/nginx/access.log
A healthy result has all three buckets close together. The characteristic failure has small near 99%, medium in the eighties and large in the single digits.
Step 2 — Test for a hard size ceiling
A ceiling produces a permanent miss that does not vary with load or time. Request an object twice on each side of the suspected limit:
for url in https://example.com/assets/app.css \
https://example.com/media/trailer-4k.mp4; do
echo "== $url"
for i in 1 2; do
curl -sI "$url" | grep -iE 'x-cache|cf-cache-status|^age'
done
done
A second request with Age: 0 and a miss, every time, on the large object only, is a ceiling rather than eviction. Eviction is probabilistic and produces occasional hits; a ceiling never does.
Step 3 — Raise or work around the ceiling
In Nginx the relevant limit is the temp-file threshold, above which a response is streamed rather than stored:
location /media/ {
proxy_cache media;
# Store responses up to 512 MB instead of streaming them through
proxy_max_temp_file_size 512m;
proxy_cache_valid 200 206 30d;
}
At a CDN the ceiling is usually a plan attribute rather than a setting. Where it cannot be raised, the workaround is to stop producing objects above it — segmenting media, or splitting a monolithic bundle, converts one uncacheable object into many cacheable ones.
Step 4 — Separate the store
Even below the ceiling, large objects consume the capacity many small ones would occupy, so a single LRU store lets media evict everything else:
varnishd -s main=file,/var/lib/varnish/main.bin,120G \
-s media=file,/var/lib/varnish/media.bin,400G
sub vcl_backend_response {
if (beresp.http.Content-Type ~ "^(video|audio)/") {
set beresp.storage = storage.media;
}
}
Two stores make both hit ratios predictable and stop a release of large assets from flushing the page cache.
Step 5 — Enable range caching where media is served whole
If the requests arrive with Range headers, the object is not stored as a unit at all and whole-object hit ratio is the wrong metric. Enabling slicing gives the edge aligned blocks it can reuse, and the measurement should move to bytes served from cache rather than requests hit.
Expected Output / Verification
After the fix, the size buckets converge. The large bucket will usually remain a few points below the others — cold blocks and genuine demand for rarely-watched content are real — but the gap should be single digits rather than an order of magnitude:
small 99.1% hit (1842019 requests)
medium 96.4% hit (140228 requests)
large 91.8% hit (21044 requests)
Confirm with the origin’s own egress figure rather than the CDN dashboard: it is the number the whole exercise exists to reduce.
Edge Cases
- A ceiling at the shield but not the edge. Different tiers can enforce different limits, producing an object that caches at one and streams through the other. Test both tiers independently.
- Compression pushing an object over the line. The stored size is the encoded size, so a poorly compressible large file can exceed a ceiling its uncompressed sibling clears.
- A ceiling expressed in memory rather than bytes on disk. Some caches store small objects in memory and large ones on disk, with separate limits for each; a misconfigured memory tier can reject objects the disk tier would happily hold.
- A CDN that reports block hits as object hits. Once range caching is enabled, a single object request can register as dozens of block-level hits, which inflates the request hit ratio for exactly the objects that were previously the problem. Read bytes served from cache alongside it, or the fix will look more effective than it is.
- Objects that are large only occasionally. An endpoint whose response size varies with the query can cross the ceiling for some inputs, producing an intermittent miss that looks like a caching bug rather than a size limit.
Frequently Asked Questions
Is there a standard maximum cacheable object size?
No. Every implementation sets its own, and the defaults are frequently far below modern asset sizes. Nginx streams responses past a buffering threshold instead of storing them, and most CDNs impose a plan-level ceiling that is worth checking against your largest files.
Why does a large object miss even when the store has free space?
Because the ceiling is a per-object rule, not a capacity rule. A response above the limit is streamed through on every request regardless of how empty the store is, which produces a permanent miss that looks nothing like eviction.
Does splitting a large file into parts help?
Substantially. Each part becomes an ordinary object well under any ceiling, and partial demand only warms the parts anyone actually requests. This is the reasoning behind segmented media formats.
Should large objects have their own cache store?
Where the cache supports it, yes. A shared store lets a handful of large objects evict thousands of small ones, and separating them makes both hit ratios predictable.
Related
- Range requests and partial content caching explains the block-level storage model that changes how large media is counted.
- Sizing a Varnish or Nginx cache store gives the measurement that decides whether eviction is the mechanism at work.
- Interpreting X-Cache and CF-Cache-Status headers is how to read the miss each mechanism produces.