skip to content

In Django, how does ManifestStaticFilesStorage bust browser caches for static files, and what is stored in staticfiles.json?

level: middleimportance: must knowfreq 55%

answer

  1. content decides the name
  2. a second copy beside the original
  3. MD5, twelve hex characters
  4. name map loaded once per process
  5. DEBUG skips the lookup

basics

~20 s

ManifestStaticFilesStorage saves, during collectstatic, a copy of each static file named with a 12-character MD5 content hash and records original-to-hashed names in staticfiles.json; {% static %} resolves through that map, so a changed file gets a new URL.

solid answer

~40 s

You enable it by setting the `BACKEND` of `STORAGES['staticfiles']` to `django.contrib.staticfiles.storage.ManifestStaticFilesStorage`. When `collectstatic` runs, the storage's `post_process()` hashes each collected file's content (MD5, first 12 hex characters) and saves a second copy such as `css/app.55e7cbb9ba48.css` next to the original, rewriting references inside CSS on the way. It then writes `staticfiles.json` into `STATIC_ROOT`: a `paths` map from `css/app.css` to the hashed name, plus a format `version` and an overall `hash`. At runtime `{% static 'css/app.css' %}` calls the storage's `url()`, which reads the map loaded when the storage object was created, so nothing is hashed per request. Because the name changes whenever the bytes change, the files can be cached far into the future and a deploy still reaches every browser. With `DEBUG = True` the lookup is skipped and unhashed URLs are rendered.

code

python · 9 lines
python
# settings.py
STORAGES = {
    'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
    'staticfiles': {
        'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage',
    },
}
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

go deeper

for a junior

Recall that the backend is chosen in STORAGES['staticfiles'], that collected files gain a content hash in their names, and that templates must use the static tag to get those names.

for a middle

Explain the pipeline: copy, hash with MD5 cut to 12 characters, rewrite CSS references, write staticfiles.json, then resolve names from an in-memory map at render time.

for a senior

Show the operational edges: DEBUG hides the lookup, the manifest loads once per process, hand-written static paths bypass it, and old hashed copies must stay deployed for cached pages.

for a principal

Weigh Django's build-time hashing against a front-end bundler that already emits hashed names, and decide which tool owns asset naming so the two never disagree.

## Why static files need new names when they change Browsers, proxies and CDNs cache **static assets** (CSS, JavaScript, images, fonts) aggressively. If `css/app.css` is served with a long cache lifetime and you then change it, visitors keep the old stylesheet until their copy expires. Short lifetimes fix freshness but cost a revalidation round trip on every page view. **Cache busting** resolves the tension: give every version of a file its own URL, let each URL be cached for as long as you like, and change the HTML so it points at the new URL. Django's built-in implementation is `django.contrib.staticfiles.storage.ManifestStaticFilesStorage`. ## Enabling it Static files are handled by whatever storage is registered under the `staticfiles` alias of the `STORAGES` setting. The default backend, `StaticFilesStorage`, copies files verbatim. Switching the backend turns hashing on: ```python STORAGES = { 'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'}, 'staticfiles': { 'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage', }, } ``` The documentation lists three requirements for hashed URLs to appear: the backend is set as above, `DEBUG` is `False`, and `collectstatic` has been run. ## What collectstatic produces 1. `collectstatic` copies every file the finders locate into `STATIC_ROOT` under its **original name**. 2. It then calls the storage's `post_process()`. For each file, `file_hash()` computes an **MD5 of the content** and keeps the first 12 hex characters, which are inserted before the extension: `css/app.css` becomes `css/app.55e7cbb9ba48.css`. The hashed copy is saved **alongside** the original. 3. CSS files are **rewritten first**: `url()` and `@import` references to other static files are replaced with their hashed names, and the CSS file's own hash is taken from the rewritten content. A stylesheet therefore gets a new name when an image it references changes. 4. Finally `save_manifest()` writes `staticfiles.json`. ## The manifest ```json {"paths": {"css/app.css": "css/app.55e7cbb9ba48.css", "img/logo.png": "img/logo.27e20196a850.png"}, "version": "1.1", "hash": "a3f2c19e8b04"} ``` - `paths` maps each original relative name to its hashed relative name. - `version` is the manifest format. Django reads `1.0` and `1.1`; any other content raises `ValueError` ("Couldn't load manifest"). - `hash` is one digest over the whole map, exposed as the storage's `manifest_hash` attribute. It changes whenever any file changes, which is handy for telling a single-page app that a new deploy is live. - The file lives in `STATIC_ROOT` by default; a subclass can pass a different `manifest_storage` to keep it elsewhere. - Since Django 6.0 the paths are written in a stable sorted order, so two builds of the same files produce the same manifest. ## How a URL is resolved at runtime `{% static %}` (and `staticfiles_storage.url()` in Python) ask the storage for a URL. What happens depends on `DEBUG`: | | `DEBUG = True` | `DEBUG = False` | |---|---|---| | URL for `css/app.css` | `/static/css/app.css` | `/static/css/app.55e7cbb9ba48.css` | | Consults the manifest | no | yes, the in-memory map | | Name missing from the manifest | unhashed URL rendered | `ValueError` (because `manifest_strict` is `True`) | The manifest is read **once**, when the storage object is created on first use in a process, and kept in memory. That is why there is no hashing cost per request, and also why a process started before `collectstatic` rewrote the file keeps using the old map until it is restarted. ## Operational consequences - **Hardcoded paths defeat it.** A template that writes `/static/css/app.css` by hand never goes through the manifest, so it serves the unhashed original and loses cache busting silently. - **Old hashed copies survive.** `collectstatic` does not delete files from earlier runs unless you pass `--clear`, so HTML cached before the deploy can still load the assets it names. - **Django only produces the names.** Sending far-future cache headers is the job of whatever serves `STATIC_ROOT`. - **The hash function is replaceable** by overriding `file_hash()` in a subclass. - **Tests run with `DEBUG = False`**, so a test that renders `{% static %}` needs either a collected manifest or a plain `StaticFilesStorage` in test settings.

  • Why is it safe, and useful, that hashed copies from the previous deploy are still in STATIC_ROOT?
    HTML cached by a proxy or an open browser tab still names the previous hashed files. Because `collectstatic` only deletes old files when run with `--clear`, those URLs keep resolving. Names never collide, since different content yields a different hash, so old and new versions coexist until you prune them deliberately.
  • Does a running Django process pick up a staticfiles.json that collectstatic rewrote after the process started?
    No. `ManifestStaticFilesStorage` loads the manifest in its constructor, and the `staticfiles` storage is created once per process on first use. Later lookups read only that in-memory map. Run `collectstatic` before the application processes start, or restart them afterwards.
  • How could a single-page app front end learn that the static assets changed after a deploy?
    Expose `staticfiles_storage.manifest_hash`, for example in a small JSON endpoint or a template variable. It is one digest computed over the whole path map when the manifest is saved, so it changes whenever any collected file changes; the front end can compare it with the value it loaded with.

It works like a library that files every new edition of a book under a new shelf mark and keeps a catalogue card per title: readers always look up the card, so they never pick up last year's edition by mistake.

saying these in an interview costs you the question

  • The hash in the file name is the file's modification time or a build number.
  • Django hashes each file on every request when rendering the static tag.
  • You enable it in Django 6.1 by setting STATICFILES_STORAGE.
  • Hashed URLs appear in development with DEBUG = True as well.
  • The manifest is re-read on every request, so no restart is needed after collectstatic.