In Django 6.1, when is the cached template loader active, and what changes if you set OPTIONS['loaders'] in TEMPLATES yourself?
answer
- compile once per process
- on by default in development too
- the autoreloader resets it
- explicit loaders replace the default
- shared nodes across threads
basics
~20 sWith DjangoTemplates and no OPTIONS['loaders'], Django wraps its filesystem and app-directories loaders in cached.Loader in every environment; setting loaders yourself replaces that default, forbids APP_DIRS, and caches only if you wrap the list in the cached loader.
solid answer
~40 sIf `OPTIONS["loaders"]` is not set, the `DjangoTemplates` engine builds `filesystem.Loader` (plus `app_directories.Loader` when `APP_DIRS` is true) and wraps them in `cached.Loader` — in development as well as production since Django 4.1. The cached loader keeps each **compiled** `Template` in memory per process and also remembers misses, so a template is read and parsed once. Under `runserver` the autoreloader watches template directories and resets the loaders when a template changes; in production each worker keeps its cache until it restarts, so template edits need a deploy or restart. Setting `loaders` yourself replaces the whole default: `APP_DIRS` must then be omitted (Django raises `ImproperlyConfigured`), and nothing is cached unless you wrap your list in `("django.template.loaders.cached.Loader", [...])`. Because compiled nodes are shared across threads, custom tag nodes must not keep per-render state on `self`.
code
python · 6 lines# ImproperlyConfigured: app_dirs must not be set when loaders is defined.
TEMPLATES = [{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
"OPTIONS": {"loaders": ["django.template.loaders.filesystem.Loader"]},
}]go deeper
Remember that Django caches compiled templates by default, so a production template change needs a deploy or restart.
Explain the default loader list, the cached wrapper, why runserver still picks up edits, and the APP_DIRS conflict with explicit loaders.
Configure custom loaders without losing caching, keep dynamic loaders outside the cache, and review custom tags for per-render state stored on nodes.
Treat templates as deployable code: decide whether any runtime-editable templates are worth their cache-invalidation complexity.
## What a template loader does A **template loader** turns a template name into a compiled `Template`. The built-in loaders include the filesystem loader (`DIRS`), the app-directories loader (`<app>/templates/`), a `locmem` loader for templates held in a dictionary, and the **cached loader**, which wraps other loaders. Compiling means reading the file and parsing it into a tree of `Node` objects, and that work is repeated on every render unless something keeps the result. ## The default configuration In `django/template/engine.py`, when the `loaders` option is `None`, the engine builds: ```python loaders = ["django.template.loaders.filesystem.Loader"] if app_dirs: loaders += ["django.template.loaders.app_directories.Loader"] loaders = [("django.template.loaders.cached.Loader", loaders)] ``` So the cached loader is **on by default in every environment**. Before Django 4.1 it was enabled only when `DEBUG` was off; the 4.1 release made it the default in development too. ## What the cached loader caches - The **compiled `Template`** for each name (and each `extends` chain position), held in memory in that process. - **Misses**: a name that raised `TemplateDoesNotExist` is remembered too, so repeated lookups of absent names (common with `select_template` fallbacks) stay cheap. The cache is **per process**. With several worker processes each has its own copy, and nothing is shared or invalidated across them. ## Development versus production | Situation | Effect of a template edit | |---|---| | `runserver` with its autoreloader | the autoreloader watches template directories and calls `reset()` on the loaders, so the next render recompiles | | production workers | the compiled template stays cached until the process restarts | | explicit `loaders` without the cached wrapper | every render re-reads and re-parses, so edits appear immediately, at a CPU cost | That is why a template hot-fix copied onto a production server appears to do nothing: the workers still hold the old compiled version. Deploy and restart instead of editing in place. ## Setting `loaders` yourself You set `OPTIONS["loaders"]` to add a custom loader (for example one that reads templates from the database) or to control ordering. Three consequences: 1. It **replaces** the default list entirely, including the cached wrapper. 2. **`APP_DIRS` must not be set**: Django raises `ImproperlyConfigured` with "app_dirs must not be set when loaders is defined." List `app_directories.Loader` explicitly instead. 3. To keep caching, wrap your loaders yourself: ```python TEMPLATES = [{ "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "OPTIONS": { "loaders": [ ("django.template.loaders.cached.Loader", [ "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", "stores.loaders.StoreTemplateLoader", ]), ], }, }] ``` A loader that reads from the database interacts badly with caching: once cached, database edits are invisible until restart. Either leave that loader outside the cached wrapper or build your own invalidation. ## Thread safety of cached templates With caching, one compiled `Template` — and every `Node` in it — is reused by all requests in the process, including concurrently in threaded servers. Django's documentation notes that all built-in tags are safe with the cached loader, but a custom tag whose `Node` stores per-render state on `self` (a counter, an iterator) will leak state between requests. Such state belongs in `context.render_context`. ## Summary checklist - Leave `loaders` unset unless you need a custom loader. - If you set it, drop `APP_DIRS` and wrap the list in `cached.Loader`. - Treat production templates as code: change them by deploying, not by editing files on the server. - Review custom tags for state kept on the node.
- A custom tag counts rows by incrementing self.count in its Node's render(). What goes wrong with the cached loader?The compiled `Node` is created once and reused by every request in the process, so the counter keeps growing across renders and concurrent threads interleave updates. Per-render state belongs in `context.render_context`, which is scoped to one template rendering.
- You add a loader that reads storefront templates from the database. Where should it sit relative to the cached loader?Outside it, unless you add invalidation. Inside `cached.Loader` the first compiled version stays in each process's memory, so edits made in the database are ignored until the workers restart.
The cached loader is a restaurant kitchen that preps each recipe once at opening and reuses the prep all night. The runserver autoreloader is a cook who re-preps whenever the recipe card changes; production kitchens only re-prep when the shift restarts.
saying these in an interview costs you the question
- The cached loader is only enabled when DEBUG is False.
- Editing a template file on a production server takes effect on the next request.
- Setting OPTIONS['loaders'] keeps Django's cached wrapper automatically.
- APP_DIRS and an explicit loaders list can be combined freely.
- The template cache is shared across all worker processes.