Caching Video Byte Ranges at the Edge
Problem Statement
A video library is served from a CDN and the origin’s egress scales with concurrent viewers rather than with catalogue size — which is the opposite of what a CDN is for. Video is the one content type where default caching behaviour is usually wrong, because players do not fetch whole files.
Prerequisite Concepts
- Range requests and partial content caching — why a
206is harder to store than a200. - Why large objects miss more often at the edge — the size-related mechanisms that compound the problem.
- How CDN cache keys are generated — the key composition that slicing depends on.
Step-by-Step Resolution
Step 1 — Establish how the player actually fetches
Everything downstream depends on this. Sample the access log for the media path and count requests carrying a Range header:
awk '/\/media\// {n++; if ($0 ~ /Range:/) r++} END {
printf "%d media requests, %d ranged (%.0f%%)\n", n, r, 100*r/n }' access.log
Predominantly ranged traffic means a player fetching windows of a whole file. Predominantly unranged traffic with many distinct URLs means a segmented format, and the problem is already solved.
Step 2 — Prefer segmentation
Where you control packaging, segmenting removes the problem rather than managing it:
GET /media/lecture-04/1080p/seg-00042.m4s HTTP/2
HTTP/2 200
Content-Length: 1874320
Cache-Control: public, max-age=31536000, immutable
ETag: "seg42-5f8a3c"
Each segment is a complete object well under any size ceiling, fingerprinted so it never needs invalidating, and cacheable at every tier without a single vendor feature being enabled.
The manifest is the one part that must stay fresh:
GET /media/lecture-04/master.m3u8 HTTP/2
HTTP/2 200
Cache-Control: public, max-age=0, s-maxage=10
Step 3 — Enable block-aligned range caching where segmentation is impossible
proxy_cache_path /var/cache/media levels=1:2 keys_zone=media:64m
max_size=400g inactive=30d use_temp_path=off;
location /media/ {
slice 1m;
proxy_cache media;
proxy_cache_key $uri$slice_range;
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 30d;
proxy_http_version 1.1;
add_header Accept-Ranges bytes always;
}
The $slice_range in the key is what makes each block an independent, reusable entry. Without it every block collides on one key.
Step 4 — Align the block size across tiers
This is the step that decides whether a shield is worth having for media. If the edge slices at one megabyte and the shield stores eight-megabyte blocks, the edge’s requests never match anything the shield holds:
Step 5 — Verify the origin scales with content, not viewers
The definitive test is to increase concurrency and watch the origin:
# Fetch the same block from several PoPs and confirm only one origin fetch results
for i in $(seq 1 20); do
curl -sI -H 'Range: bytes=10485760-11534335' \
https://example.com/media/lecture-04.mp4 | grep -i 'cf-cache-status' &
done; wait
Nineteen hits and one miss is the correct shape. Twenty misses means ranges are being forwarded and the origin is absorbing viewer concurrency directly.
Expected Output / Verification
Correct configuration produces a 206 with a non-zero Age on repeated block requests, hits on offset ranges within the same block, and an origin byte count that stays flat as viewer count rises.
HTTP/2 206
Content-Range: bytes 10485760-11534335/524288000
Age: 8412
CF-Cache-Status: HIT
Edge Cases
- A size ceiling below the file size. Some tiers refuse to store objects above a limit entirely, which disables slicing at that tier regardless of configuration.
- Signed URLs changing per viewer. A token in the query string puts every viewer on a different cache key. Sign a path prefix, or exclude the token from the key, or nothing will ever be shared.
- The manifest cached as long as the segments. A frozen manifest keeps pointing at segments that have rotated away. Give it a short shared lifetime and the segments a long one.
- Origin not honouring
Range. Slicing issues block requests to the origin; if the origin ignores them it returns the whole file for every block, which is worse than forwarding. - Mixed bitrates multiplying the working set. Every rendition is a separate object set. Storage and shield sizing must account for the full ladder, not the source file.
- Live streams treated like recordings. A live manifest changes every few seconds and must not inherit the recorded-content lifetime, or players will stall on a stale segment list.
Frequently Asked Questions
Is segmented delivery always better than range caching?
For cacheability, yes — every segment is an ordinary object and no partial-content logic is involved. The cost is a packaging step and a manifest, which is not always available for user-uploaded or third-party content. Where packaging is possible it is the simpler system.
What slice size should I use?
One megabyte is a common default and works well for most video bitrates. Smaller slices improve seek granularity at the cost of more requests; larger slices reduce request count but waste bandwidth on short seeks. The important property is that every tier uses the same value.
Does slicing increase origin request count?
Initially, yes — a cold file is fetched in blocks rather than as one object. Steady-state origin traffic falls sharply, because blocks are reused across viewers while a forwarded arbitrary range almost never is.
Why does the shield not help with video?
Usually because the block boundaries differ between the tiers. If the edge requests one-megabyte blocks and the shield stores eight-megabyte blocks, the edge’s requests never align with anything the shield holds and every one becomes an origin fetch.
Related
- Origin shielding and request collapsing is the tier whose value depends entirely on block alignment for media.
- Cache storage, eviction and capacity explains why media deserves its own store rather than sharing one.
- Caching hashed static assets with immutable covers the directive set a fingerprinted segment should carry.