skip to content

Which Cache-Control headers does WhiteNoise send for hashed versus unhashed Django static files, and how does it tell them apart?

level: middleimportance: should knowfreq 40%

answer

  1. forever versus one minute
  2. strip the hash, ask the storage
  3. the manifest is the proof
  4. WHITENOISE_MAX_AGE for the rest

basics

~20 s

WhiteNoise marks a file immutable, max-age=315360000, public, immutable, when stripping its hash and asking Django's staticfiles storage for the URL gives back the same name; other files get max-age=60, public by default (0 under DEBUG).

solid answer

~40 s

For each file, `WhiteNoiseMiddleware` runs an immutability test: it removes the last dot-separated segment before the extension (`app.55e7cbb9ba48.css` becomes `app.css`), calls `staticfiles_storage.url('app.css')`, and treats the file as versioned if the returned URL's basename equals the requested one. Versioned files get `Cache-Control: max-age=315360000, public, immutable`, ten years. Everything else gets `max-age=<WHITENOISE_MAX_AGE>, public`, where the default is 60 seconds, or 0 when `DEBUG` is on; `None` omits the header. The test relies on a manifest-backed storage such as `CompressedManifestStaticFilesStorage`: with plain `StaticFilesStorage`, or with `DEBUG` on, `url()` returns unhashed names and nothing is ever immutable. Assets hashed by a front-end bundler need `WHITENOISE_IMMUTABLE_FILE_TEST`, a function or regex.

code

python · 5 lines
python
# settings.py: treat bundler output such as main.3f9a1c2b.js as immutable too
WHITENOISE_IMMUTABLE_FILE_TEST = r'^.+\.[0-9a-f]{8,12}\..+$'

# keep unversioned files fresh for five minutes instead of one
WHITENOISE_MAX_AGE = 300

go deeper

for a junior

Recall that hashed static files get a ten-year immutable Cache-Control and others a short max-age, 60 seconds by default.

for a middle

Explain the immutability test: strip the hash, ask staticfiles_storage.url(), compare basenames, and why that needs a manifest-backed storage.

for a senior

Diagnose assets stuck on short caching, such as plain StaticFilesStorage, hardcoded paths or bundler hashes, and fix them with the right storage or WHITENOISE_IMMUTABLE_FILE_TEST.

for a principal

Set the caching policy for unversioned files against deploy frequency and CDN load, knowing only versioned URLs can be cached safely for long.

## Two kinds of static file A Django project collected with a manifest-backed storage has two copies of most assets in `STATIC_ROOT`: the original (`css/app.css`) and a **versioned** copy whose name includes a content hash (`css/app.55e7cbb9ba48.css`). A versioned URL can never point at different bytes, so it can be cached indefinitely. An unversioned URL can change content on the next deploy, so it should be cached only briefly. WhiteNoise sets `Cache-Control` accordingly. ## The headers | File | `Cache-Control` sent | Controlled by | |---|---|---| | Versioned (hash in the name, confirmed by the storage) | `max-age=315360000, public, immutable` | fixed: WhiteNoise's `FOREVER`, ten years | | Unversioned, `DEBUG = False` | `max-age=60, public` | `WHITENOISE_MAX_AGE` (default 60) | | Unversioned, `DEBUG = True` | `max-age=0, public` | `WHITENOISE_MAX_AGE` default under `DEBUG` | | Unversioned, `WHITENOISE_MAX_AGE = None` | no `Cache-Control` header | explicit opt-out | The ten-year figure follows what a common web server emits for its maximum expiry. The 60-second default is chosen to avoid serving stale unversioned files for long while still letting a CDN absorb load. ## How WhiteNoise decides a file is versioned The default `immutable_file_test` of `WhiteNoiseMiddleware` does not trust the file name alone: 1. It ignores URLs outside the static prefix. 2. It strips one dot-separated segment before the extension: `css/app.55e7cbb9ba48.css` becomes `css/app.css`. If nothing changes, the file is not versioned. 3. It calls `staticfiles_storage.url('css/app.css')`, the same lookup `{% static %}` performs. 4. If the basename of that URL equals the basename of the requested URL, the file is versioned; if the lookup raises `ValueError` or returns a different name, it is not. So the **manifest** is the proof. A file called `jquery.min.js` is not mistaken for a hashed file, because the storage maps `jquery.js` to something else or to nothing. ## When nothing becomes immutable - **Plain `StaticFilesStorage`**: `url()` returns unhashed names, so step 4 never matches and every file gets the short max-age. - **`DEBUG = True`**: Django's manifest storage skips hashing in `url()`, so again nothing matches. That is intended; development should not cache aggressively. - **Hashes added by a front-end bundler**: names like `main.3f9a1c.js` are not in Django's manifest. Set `WHITENOISE_IMMUTABLE_FILE_TEST` to a function taking `(path, url)` or to a regular expression matched against the URL. ## Other headers WhiteNoise adds - `Last-Modified` and an `ETag` derived from modification time and size, so conditional requests can get `304 Not Modified`. - `Vary: Accept-Encoding` when compressed variants exist for the file. - `Access-Control-Allow-Origin: *` by default (`WHITENOISE_ALLOW_ALL_ORIGINS`), which matters when assets are served from another origin such as a CDN. - Custom headers through `WHITENOISE_ADD_HEADERS_FUNCTION`, a callable receiving `(headers, path, url)`.

  • Why does WhiteNoise confirm the hash through staticfiles_storage.url() instead of matching a hex pattern in the name?
    A pattern would misclassify files whose names happen to contain a hex-like segment, and caching such a file for ten years could strand users on stale content. Asking the storage proves the name came from the manifest for the current build, so only genuinely content-addressed files are cached forever.
  • Is raising WHITENOISE_MAX_AGE to a day a good way to speed up a Django site?
    Usually not. It only affects files that are not versioned, which are exactly the ones whose content can change without their URL changing. The real fix is to reference assets through `{% static %}` with a manifest-backed storage so they become versioned and get the ten-year immutable header.

saying these in an interview costs you the question

  • WhiteNoise marks any file with a hex-looking segment in its name as immutable.
  • Every static file WhiteNoise serves is cached for ten years.
  • Plain StaticFilesStorage is enough for WhiteNoise's immutable headers.
  • WHITENOISE_MAX_AGE sets the lifetime of hashed files.
  • Immutable headers also appear in development with DEBUG on.