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:

  1. Client requests full file, gets it cached with immutable headers
  2. If download is interrupted before caching completes, client sends Range: bytes=<offset>- to resume
  3. Server responds with 206 Partial Content and Content-Range: bytes <offset>-<end>/<total>
  4. Once complete, the full file is cached and the immutable directive 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:

  1. Pass-through: The CDN forwards range requests to origin and caches the full file on first complete request
  2. 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-Range headers
  • 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

  1. Request the archive and inspect response headers for Cache-Control: public, max-age=31536000, immutable
  2. Verify ETag is present and strong (no W/ prefix)
  3. Verify Accept-Ranges: bytes is present
  4. Test range request: curl -r 0-1023 -I https://example.com/downloads/archive-v1.0.tar.gz → expect 206
  5. Test resume: interrupt a download, resume with curl -C -, verify it completes correctly
  6. 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.gz that always points to the newest release) — use max-age=3600, must-revalidate instead
  • Pre-release / nightly builds that may be replaced — use shorter max-age with revalidation
  • Files you might need to recall for security reasons — immutable makes cache invalidation impossible on the client side; you would need to change the URL

Related reading on wplus.net