8.5 Slice and Large Files: Caching for Media
A 10 GB video in one cache entry is a 10 GB hole in your disk and a 10 GB wait for the first byte. The slice module cuts big content into cacheable, parallel, resumable pieces.
Intermediate
What you will learn
Why large files break naive caching (memory, latency, waste).
slice— the chunked-fetch architecture, and its two mandatory companions.Range requests through a sliced cache — 206 receipts included.
What's on disk after slicing (the 1 MB entries, verified).
Tuning: chunk size,
proxy_max_range, and when not to slice.
1. Why large files break naive caching
Serve a 2 GB file through the default proxy cache and three things happen:
The single-entry problem. The whole body must stream to disk before the entry is usable for anyone else (atomic rename, Lesson 8.2) — concurrent viewers of a cold file all wait for one complete download, or trigger one each (no partial sharing).
The partial-consumption problem. A viewer who watches 3 minutes of your 2 GB cache entry cost you a 2 GB origin fetch and a 2 GB disk allocation — 97% pure waste.
The seek problem. A client scrubbing to minute 40 sends
Range: bytes=…; your cache either ignores the range (fetches everything, seeks) or bypasses caching for ranged requests (origin storms).
Range requests are the client's native language for media — every video player, downloader, and resumable-fetch speaks them. A cache that treats content as all-or-nothing speaks the wrong one.
2.
slice — the architecture
location /sliced/ {
proxy_pass http://127.0.0.1:8800/largefiles/;
proxy_cache cache;
slice 1m; # ← the directive
proxy_set_header Range $slice_range; # ← companion 1
proxy_cache_key "$scheme$proxy_host$request_uri$slice_range"; # ← 2
proxy_cache_valid 200 206 1h; # ← 3 (206s are NOT cached by default!)
}What
slice 1m does to a request for
/sliced/file-10m.bin:
client GET /sliced/file-10m.bin (no Range)
└─ NGINX splits into 10 subrequests, each with Range: bytes=N-(N+1M)
├─ slice 0: bytes=0-1048575 ┐
├─ slice 1: bytes=1048576-2097151 │ each fetched INDEPENDENTLY,
├─ … │ cached under its OWN key,
└─ slice 9: bytes=9437184-10485759 ┘ streamed to the client as they landThe three companions are mandatory understanding:
Range $slice_range— the subrequests carry slice-aligned ranges to the origin (the client's arbitrary range gets quantized to chunk boundaries).The key gains
$slice_range— each chunk is its own cache entry. This is the entire point: chunks are fetched once, shared by every later request regardless of what range they asked for.proxy_cache_valid 200 206 …— origin responses to ranges are 206s, and 206s aren't cached unless you list them. A common misconfiguration: nothing cached until the directive appears) — if a sliced cache stores nothing, this is why.
3. Range requests through the sliced cache
The receipts, both directions:
# full file through the slice pipeline:
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8800/sliced/file-10m.bin
200
# a mid-file range, cold:
$ curl -s -o /dev/null -D - -H 'Range: bytes=2097152-2097251' \
'http://127.0.0.1:8800/sliced/file-10m.bin' | grep -Ei 'HTTP/|x-cache'
HTTP/1.1 206 Partial Content
X-Cache-Status: MISS
# the same range again:
$ curl -s -o /dev/null -D - -H 'Range: bytes=2097152-2097251' \
'http://127.0.0.1:8800/sliced/file-10m.bin' | grep -i x-cache
X-Cache-Status: HIT # the slice was already cachedTrace the second call: the client asked for 100 bytes at offset 2 MB — NGINX mapped it to
slice 2 (
bytes=2097152-3145727), fetched exactly that 1 MB chunk, served the client its 100 bytes, and stored the chunk. The repeat hit the chunk in cache. Meanwhile slices 0–1 and 3–9 were
never fetched — 20% of the origin traffic a naive full-file cache would have spent.
That's the media-serving trifecta:
seek = one chunk,
scrub = sequential chunks,
concurrent viewers = shared chunks (the herd lesson applies per-chunk — cold chunks collapse under
proxy_cache_lock).
4. What's on disk
After the slice tests, the cache directory tells the story:
$ ls -l …/476af306…
-rw------- 1049290 # 1 048 576 B slice body + 714 B metadata
# every entry in the store is slice-sized:
$ find tmp/cache -type f -printf '%s\n' | sort -u
1049290 # never the whole 10 MB fileThe largest file in the cache is a
slice: 1 048 576 bytes of body + 714 bytes of NGINX metadata (Lesson 8.2's header, now with a receipt). No 10 MB blob anywhere — disk fills with exactly the chunks audiences actually watched. Eviction (
inactive) reclaims unpopular chunks independently: a viral 30-second mark of a 2-hour file costs you ~megabytes, not gigabytes.
5. Tuning — and when not to slice
Chunk size (
slice 1m
vs
8m
vs
64m
) is the one dial:
smaller chunks → faster first byte (fetch less before serving), finer eviction granularity, more entries (zone sizing!), more per-chunk overhead (request per chunk, metadata per chunk);
bigger chunks → fewer requests/entries, better sequential throughput, slower cold seeks and coarser waste.
The media industry's comfort zone is 1–8 MB for video, larger for software-distribution shapes. Rule of thumb: chunk ≈ what a typical viewer consumes in 2–5 seconds.
When not to slice:
Small content — sub-chunk files pay the machinery for nothing(a 50 KB asset becomes one partial-chunk entry either way).
Origins that dislike ranges — slice requires origin range support (static file servers, S3: yes; some app servers: check).
Already-chunked content — HLS/DASH segments are pre-sliced by design; cache them as ordinary small files (Module 3's
try_files-era patterns, plus this module's defaults).
Two neighbors worth knowing:
max_ranges (core, default unlimited) bounds how many ranges one request may carry, and
proxy_cache_max_range_offset makes range requests beyond an offset uncacheable (multi-range requests with slice behave reasonably but test your origin), and
proxy_force_ranges on — makes NGINX itself respond with 206s for
uncacheable proxied content, the polite lie that keeps client resumers working.
Try it yourself
Watch chunks appear. Request
/sliced/file-10m.bin?me=1, then find its slices:find tmp/cache -size +500k | wc -l→ ~10. Request a different mid-file range on the same URL and count again — only the new slices appeared.The seek experiment. Cold URL, request
Range: bytes=9437184-9437283(the last MB) — count cache files: exactly one slice. The first 9 MB never crossed the wire.Chunk-size A/B. Set
slice 4m, reload, fresh URL, full-file request: entries now 4 MB-sized. Feel the trade (fewer entries, slower first-byte — time it withcurl -w '%{time_starttransfer}').The 206 lesson, recreated. Remove
206fromproxy_cache_valid, reload, fresh URL, full fetch — then count +500k files in the cache: zero. The silent gotcha, personally experienced. Restore.
Q&A
Q1. Why does
$slice_range belong in the cache key, and what breaks without it?
Answer
Chunks are the cacheable units; the range identifies which chunk. Without it, all slices of a URL would collide on one key — first chunk stored, every other chunk's store attempt fights it, ranges serve wrong bytes. (The same key-granularity law as Lesson 8.2, applied at chunk grain.)
Q2. A client requests
(4 bytes straddling the slice 2/3 boundary). What does the origin see? (100 bytes straddling nothing, inside slice 3). What does the origin see?
Answer
Two slice fetches:
bytes=2097152-3145727 and
bytes=3145728-4194303 — the range crosses a 1 MiB-aligned boundary, and slices are the unit of transfer, so both chunks are fetched (and both land in cache). The client still receives exactly its 4 bytes. That is the trade the module makes: sub-slice ranges are never split on the wire to the origin — they are quantized to whole slices.
Q3. Your video origin bills per-request. Chunk size up or down — and what's the hidden cost of your choice?
Answer
Up — fewer chunk-fetches per byte watched (request count ∝ bytes / chunk). Hidden costs: slower time-to-first-byte on cold seeks, coarser waste for partial consumption, and larger single entries (eviction granularity). Balance at "a few seconds of playback."
Q4. HLS segments (2 s
.ts files) — slice or not, and why?
Answer
Not: the content is already chunked at the right granularity. Cache the segments as ordinary files (default key, normal TTLs); the manifest can even be micro-cached (Lesson 8.4). Slicing pre-chunked content adds machinery without benefit.
Takeaways
Naive large-file caching wastes disk, serializes cold fetches, and speaks the wrong language to range-speaking clients.
slicequantizes requests into chunk-aligned range subrequests, cached as independent entries — with three companions:Range $slice_range,$slice_rangein the key, and 206 listed inproxy_cache_valid(the silent gotcha, now yours forever).Seeks cost one chunk; scrubbing streams chunks; viewers share chunks; eviction reclaims chunks — disk spends on attention, not on bytes-originally-fetched.
Chunk size is the single dial (1–8 MB for media); don't slice small, range-hostile, or pre-chunked content.
Module 8 complete. Your edge now stores the right things, for the right durations, survives origin failures, absorbs stampedes, refreshes invisibly, and speaks fluent media. Next: Module 9 — the system underneath: worker tuning, kernel limits, buffers, compression (gzip/Brotli), and the HTTP/3/QUIC frontier.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.