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

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.

Two delivery models, two caching problemsSegmented delivery produces ordinary objects that any cache handles; whole-file range delivery produces partial content that most caches decline to store.Segmented (HLS, DASH)ordinary 200 responsesOne URL per segment, each a few seconds longCached by every tier with no special featuresFingerprinted segment URLs can be immutableRequires a packaging step and a manifestWhole file with ranges206 partial contentOne URL, arbitrary client-chosen byte windowsNeeds slicing or range caching to be cacheableWorks with any player and any source fileBlock alignment must match across tiers
The delivery model decides how hard the caching is

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:

Aligned blocks make the shield usefulWhen both tiers slice at the same boundary the edge's block request is exactly what the shield stores, so a second PoP requesting the same block is answered without an origin fetch.Playerarbitrary rangeRangeEdge1 MB aligned blocksame boundaryShield1 MB aligned blockonly on missOriginone fetch per block
Same boundary at both tiers, or the shield never hits

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.

What to expect from each configurationThe three viable configurations differ sharply in how origin traffic responds to viewer growth, which is the only property that matters at scale.Origin scales withCold-start costSetup effortSegmented deliverydistinct segmentslowpackaging stepSliced range cachingdistinct blocksmoderateCDN feature plus keyRanges forwardedconcurrent viewersnonenone
How origin traffic responds to more viewers

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.


Back to Range Requests and Partial Content Caching