Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Intermediate ~35 min read

Intermediate

What you will learn

  1. Why large files break naive caching (memory, latency, waste).

  2. slice — the chunked-fetch architecture, and its two mandatory companions.

  3. Range requests through a sliced cache — 206 receipts included.

  4. What's on disk after slicing (the 1 MB entries, verified).

  5. 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:

  1. 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).

  2. 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.

  3. 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

nginx
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:

text
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 land

The 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:

bash
# 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 cached

Trace 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:

text
$ 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 file

The 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

  1. 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.

  2. 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.

  3. 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 with curl -w '%{time_starttransfer}').

  4. The 206 lesson, recreated. Remove 206 from proxy_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.

  • slice quantizes requests into chunk-aligned range subrequests, cached as independent entries — with three companions: Range $slice_range, $slice_range in the key, and 206 listed in proxy_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.

Was this page helpful?

Mark this learning resource complete to track your learning progress.