Debugging 206 Partial Content Cache Behaviour

Problem Statement

Video playback is generating far more origin traffic than the catalogue size suggests it should, and the CDN dashboard reports a healthy hit ratio. Ranged requests behave differently enough from ordinary ones that both observations can be true simultaneously. This procedure establishes what is actually happening.

Prerequisite Concepts

Step-by-Step Resolution

Step 1 — Confirm range support end to end

curl -sI https://example.com/media/lecture-04.mp4 \
  | grep -iE 'accept-ranges|^etag|content-length|cache-control'

Accept-Ranges: bytes should be present, together with a quoted, non-weak ETag. A weak validator here is worth fixing before going further, because it disables If-Range and therefore every resumed download.

Then confirm a range is actually honoured:

curl -s -o /dev/null -D - -H 'Range: bytes=0-1023' \
  https://example.com/media/lecture-04.mp4 | head -6

HTTP/2 206 with Content-Range: bytes 0-1023/524288000 is correct. A 200 means the range was ignored or stripped somewhere in the path.

What each response to a range request meansFour possible answers to the same ranged request, and what each one implies about the path between the client and the origin.StatusImplicationRange honoured206 with Content-Rangeworking as intendedRange ignored200 with the full bodyorigin or proxy does not support itPrecondition failed200 after If-Rangerepresentation changed, correct fallbackRange not satisfiable416offsets exceed the resource length
Reading the answer to a range request

Step 2 — Test whether the range is stored

Issue the identical range twice and compare:

for i in 1 2; do
  curl -sI -H 'Range: bytes=1048576-2097151' https://example.com/media/lecture-04.mp4 \
    | grep -iE '^age|cf-cache-status|x-cache'
done

A second response with a non-zero Age means the fragment or its containing block is stored. A permanent MISS with Age: 0 means every range is being forwarded to the origin — which is the finding that explains an origin bill that does not match the dashboard.

Forwarded ranges versus stored blocksWithout range caching every client range becomes an origin request; with slicing the edge fetches aligned blocks once and serves arbitrary ranges from them.PlayerEdgeOriginRange: bytes=5242880-6291455forwarded verbatim, no storage206Range: bytes=5242881-6291456with slicing: same block, served locally
The same seek pattern, with and without block storage

Step 3 — Isolate the tier

Compare the origin’s own behaviour against the edge’s:

# Direct to the origin
curl -sI -H 'Range: bytes=0-1023' \
  --resolve example.com:443:203.0.113.10 https://example.com/media/lecture-04.mp4 | head -3

# Through the CDN
curl -sI -H 'Range: bytes=0-1023' https://example.com/media/lecture-04.mp4 | head -3

If the origin answers 206 and the edge answers 200, the edge is not forwarding the range. If the origin answers 200, range support is missing at the source and no CDN feature can compensate — slicing depends on the origin honouring the block requests the edge issues.

Step 4 — Establish the block granularity

A cache that stores aligned blocks behaves differently from one that stores exact ranges. Request a range offset by one byte from a previously requested one:

curl -sI -H 'Range: bytes=1048576-2097151' https://example.com/media/lecture-04.mp4 | grep -i '^age'
curl -sI -H 'Range: bytes=1048577-2097152' https://example.com/media/lecture-04.mp4 | grep -i '^age'

Two hits mean the cache is serving both from the same stored block — aligned storage, which is what you want. A hit followed by a miss means it is storing exact ranges, and a player that never repeats an exact byte window will never hit.

Step 5 — Check how the metrics are counting

Content-Length on a 206 describes the fragment. An egress calculation that sums it under-reports ranged traffic by whatever fraction the ranges represent:

# Correct: derive the resource size from Content-Range, the transfer from Content-Length
curl -sI -H 'Range: bytes=0-1023' https://example.com/media/lecture-04.mp4 \
  | awk -F'[ /]' '/[Cc]ontent-[Rr]ange/ {print "resource bytes: " $NF}'
Origin bytes for one hour of playback, by configurationForwarding ranges makes origin traffic scale with concurrent viewers; block storage makes it scale with the number of distinct blocks anyone watches.Ranges forwarded186GB scales with viewersBlocks stored, misaligned61GB partial reuseBlocks stored, aligned4GB scales with contentidentical playback behaviour in all three cases
Origin egress for 400 concurrent viewers of one file

Expected Output / Verification

A correctly configured path produces 206 at every tier, a non-zero Age on a repeated range, hits on offset ranges within the same block, and an origin byte count that tracks the number of distinct blocks watched rather than the number of viewers.

HTTP/2 206
Content-Range: bytes 1048576-2097151/524288000
Content-Length: 1048576
Age: 3241
CF-Cache-Status: HIT

Edge Cases

  • A weak ETag on a rangeable resource. If-Range requires strong comparison, so resumption always restarts. The symptom is enormous egress with no obvious configuration fault.
  • Multi-range requests. multipart/byteranges is inconsistently handled by intermediaries and rarely cached. Limiting or rejecting multi-range requests is usually better than relying on them.
  • Compression between two range requests. Offsets refer to transferred bytes, so a re-encoding invalidates every offset the client holds mid-download.
  • Mismatched block sizes between tiers. An edge slicing at one megabyte in front of a shield slicing at eight will miss at the shield on nearly every request.
  • A 416 after a deploy. If the resource shrank, previously valid offsets are now out of range. Clients holding partial copies see 416 until they restart the download.
  • Range support disabled by a security rule. Some WAF configurations strip or reject Range headers to mitigate specific attacks, which silently converts every ranged request into a full transfer.

Frequently Asked Questions

Why does a range request return 200 instead of 206?

Either the server does not support ranges for that resource, or an intermediary stripped the Range header, or an If-Range precondition failed. The third case is the most common and is a correct, designed fallback rather than an error.

Is a permanent MISS on ranged requests always a bug?

No. Several proxies deliberately decline to store partial content because combining fragments correctly is difficult. It is a bug only if you expected the tier to cache ranges, which usually means a slicing or range-caching feature that has not been enabled.

What does Content-Length mean on a 206?

The size of the fragment being sent, not of the resource. The resource size appears after the slash in Content-Range. Metrics that sum Content-Length will therefore under-report ranged egress substantially.

Can two caches disagree about block boundaries?

Yes, and it is a common cause of poor shield hit ratios on media. If the edge slices at one megabyte and the shield at eight, the edge’s requests never align with what the shield stores. Configure the same block size at every tier.


Back to Range Requests and Partial Content Caching