Versioned download archives — release tarballs, signed packages, firmware images, dataset snapshots — are published once and never change. The URL includes a version number, the contents are fixed, and any modification means a new version at a new URL. This is the ideal case for aggressive caching, and yet many hosting setups serve these files with conservative cache headers that force unnecessary revalidation, waste bandwidth, and slow down downloads.
This guide covers how to configure immutable caching correctly for download archives, pair it with strong ETags for integrity, and ensure range-request support stays solid for resumable downloads.
Why immutable caching matters for archives
Every time a client requests a versioned file and your server returns 304 Not Modified, both sides have done unnecessary work: the client sent a conditional request, waited for the round trip, and the server processed the validation. For large archive files on high-latency links, this overhead is tangible.
The immutable directive in Cache-Control tells the browser: "This resource will not change. Do not bother revalidating during navigation." Combined with a long max-age, it eliminates conditional requests entirely for cached resources.
For a package repository or download mirror serving thousands of versioned files, the aggregate bandwidth and latency savings are significant.
The correct header set
For a versioned, never-changing download archive:
Cache-Control: public, max-age=31536000, immutable
ETag: "sha256-a1b2c3d4e5f6..."
Content-Type: application/gzip
Content-Length: 248791040
Accept-Ranges: bytes
Content-Disposition: attachment; filename="project-v2.4.1.tar.gz"
Breaking down each directive
public — the response can be cached by shared caches (CDNs, proxies), not just the browser's private cache. Essential for CDN caching.
max-age=31536000 — cache for one year (the conventional maximum). Since the file is versioned and immutable, this is safe.
immutable — signals that the resource will not change during its freshness lifetime. Browsers that support this (Firefox, Chrome 100+, Safari 16+) skip revalidation even during user-initiated navigations.
ETag with content hash — a strong ETag based on the file's SHA-256 hash serves two purposes: cache validation for clients that don't support immutable, and integrity verification. Use strong ETags (no W/ prefix) — weak ETags cannot be used with range requests.
Accept-Ranges: bytes — explicitly advertises that the server supports byte-range requests, enabling download resume.
Content-Length — required for range requests to work correctly and for download managers to show progress.
ETag strategies for archives
Content-hash ETags (recommended)
Generate the ETag from the file's cryptographic hash:
ETag: "sha256-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
Advantages:
- Changes if and only if the content changes
- Consistent across multiple servers (no server-specific state)
- Doubles as an integrity check
Filesystem-derived ETags (acceptable)
Some servers generate ETags from inode, size, and modification time:
ETag: "5f8b3c-ed3a000-5e18b740"
This works but has downsides:
- Not consistent across servers (different inodes)
- Does not provide integrity verification
- Can change if the file is re-deployed with the same content but different metadata
For download archives where integrity matters, content-hash ETags are worth the computational cost.
Range requests and immutable caching
Range requests and immutable caching work together naturally:
- Client requests full file, gets it cached with
immutableheaders - If download is interrupted before caching completes, client sends
Range: bytes=<offset>-to resume - Server responds with
206 Partial ContentandContent-Range: bytes <offset>-<end>/<total> - Once complete, the full file is cached and the
immutabledirective prevents future revalidation
Critical requirements for range requests
- Strong ETag: Clients use
If-Range: "<etag>"to ensure the file hasn't changed between the initial request and the resume. Weak ETags are rejected for this purpose. - Correct
Content-Length: Must reflect the full file size, not the range. Accept-Ranges: bytes: Must be present in the initial response.
CDN behaviour with range requests
CDNs typically handle range requests in one of two ways:
- Pass-through: The CDN forwards range requests to origin and caches the full file on first complete request
- Slice caching: The CDN caches the file in fixed-size slices and assembles range responses from cached slices
For immutable archives, either approach works. But verify:
- Your CDN respects
Cache-Control: immutable(most modern CDNs do) - Range requests to cached content return correct
Content-Rangeheaders - Sliced caching doesn't introduce gaps for very large files (test with files > 1 GB)
Implementation patterns
Nginx configuration
location /downloads/ {
# Versioned archives — immutable caching
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Accept-Ranges bytes always;
# Generate ETag from content (nginx does this by default from file metadata)
etag on;
# For content-hash ETags, use a map or Lua script
# to set ETag from a sidecar .sha256 file
}
Caddy configuration
example.com {
handle /downloads/* {
header Cache-Control "public, max-age=31536000, immutable"
header Accept-Ranges "bytes"
file_server
}
}
Cloudflare Pages / CDN
If serving through Cloudflare, set cache headers via _headers file or Page Rules. Cloudflare respects Cache-Control: immutable at the edge.
Common mistakes
Using no-store or no-cache for versioned files. These directives are for dynamic content. Versioned archives should use immutable with long max-age.
Forgetting Content-Disposition for direct downloads. Without it, browsers may try to render the file (especially for .zip or other recognized types) instead of triggering a download.
Setting max-age without immutable. Browsers may still revalidate on navigation without immutable. The combination of both is what eliminates unnecessary round trips.
Regenerating files with the same name but different content. If you update project-v2.4.1.tar.gz in place, every cache between you and the user will serve the old version for up to a year. Immutable caching assumes immutable content — any change must go to a new URL.
Weak ETags with range requests. If your server generates W/"..." ETags, range-request clients that use If-Range will not get a partial response — they'll get the full file again. Ensure strong ETags for any resource that supports ranges.
Verification checklist
- Request the archive and inspect response headers for
Cache-Control: public, max-age=31536000, immutable - Verify
ETagis present and strong (noW/prefix) - Verify
Accept-Ranges: bytesis present - Test range request:
curl -r 0-1023 -I https://example.com/downloads/archive-v1.0.tar.gz→ expect206 - Test resume: interrupt a download, resume with
curl -C -, verify it completes correctly - After caching, navigate to the URL in a browser and check DevTools — no conditional request should fire (look for "disk cache" in the Size column)
When not to use immutable caching
- Mutable files at stable URLs (e.g.,
latest.tar.gzthat always points to the newest release) — usemax-age=3600, must-revalidateinstead - Pre-release / nightly builds that may be replaced — use shorter
max-agewith revalidation - Files you might need to recall for security reasons —
immutablemakes cache invalidation impossible on the client side; you would need to change the URL
Related reading on wplus.net
- HTTP/3 for Large Downloads — transport-layer changes for large file delivery
- HTTP/3 Hosting Checklist for 2026 — QUIC rollout fundamentals
- Hosting hub — hosting architecture overview