skip to content

In a Django site served by several worker processes, which parts of the request path are built once per process rather than per request, and what bugs follow?

level: seniorimportance: should knowfreq 34%

answer

  1. import time versus request time
  2. setup and the handler constructor
  3. middleware __init__ runs once
  4. the resolver is cached
  5. class-based views are fresh

basics

~20 s

Settings, the app registry, the handler with its middleware instances and the cached URL resolver are built once per process. Request state stored on middleware instances, or values computed at import time, therefore leaks between requests or goes stale.

solid answer

~40 s

`get_wsgi_application()` (or `get_asgi_application()`) runs `django.setup()` once per process: settings, logging config, the app registry, every `AppConfig.ready()`. Constructing the handler calls `load_middleware()`, which instantiates each middleware class **once**, so its `__init__` is one-time and the instance serves every request that process handles. The resolver for `ROOT_URLCONF` is cached, and the URLconf module is imported on first use and kept. Per request you get a new `HttpRequest`, a fresh class-based view instance from `as_view()`, and a view call. The bugs: storing the current user on `self` in a middleware leaks it across requests and threads; code evaluated at import time in `urls.py` or `views.py` (a queryset turned into a list, `date.today()`) goes stale until the process restarts; module-level counters differ per worker.

code

python · 21 lines
python
class CurrentRecipeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response  # runs once per process

    def __call__(self, request):
        # BUG: shared across every request this process serves
        self.slug = request.GET.get("recipe")
        response = self.get_response(request)
        response["X-Recipe"] = self.slug or ""
        return response


class FixedRecipeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        slug = request.GET.get("recipe")  # local to this request
        response = self.get_response(request)
        response["X-Recipe"] = slug or ""
        return response

go deeper

for a junior

Remember that module-level code runs when the module is imported, not on every request, and that middleware init runs once.

for a middle

List what django.setup() and load_middleware() build once per process, and contrast it with the fresh request object and CBV instance per request.

for a senior

Diagnose leaked or stale data from its lifetime: middleware attributes under threads, import-time queries in urls.py, per-worker module caches, and prescribe the fix for each.

for a principal

Set team rules for process-lifetime state: what may live at import time, where shared state must go, and how deploys and restarts refresh it.

## Two lifetimes in one request path Every Django request path mixes objects with two very different lifetimes. Mixing them up produces bugs that pass every test (one process, one request at a time) and only appear under production concurrency, often as one user seeing another user's data. ## Built once per process | Object | When it is built | Lives until | |---|---|---| | settings and logging config | `django.setup()`, called by `get_wsgi_application()` / `get_asgi_application()` | process exit | | app registry, models, `AppConfig.ready()` side effects | `apps.populate()` inside `django.setup()` | process exit | | handler and middleware **instances** | handler constructor calls `load_middleware()` | process exit | | URL resolver for `ROOT_URLCONF` | first call to `get_resolver()`, cached with `functools.cache` | process exit | | URLconf module and `views` modules it imports | lazily, the first time the resolver needs its patterns | process exit | Django's middleware documentation states it directly: unlike `__call__()`, which runs once per request, `__init__()` runs only once. "Once" means once per process that builds the handler, so each worker process has its own instances. ## Built per request - the `WSGIRequest` or `ASGIRequest` object; - the thread-local URLconf set by `set_urlconf()` at the start of `get_response()`; - a **new instance** of a class-based view: the function returned by `as_view()` does `self = cls(**initkwargs)` on every call; - the response and anything the view computes inside its body. ## The bugs that follow 1. **Per-request state on a middleware instance.** A middleware that does `self.user = request.user` in `__call__` writes to an object shared by every request the process serves. Under threads, request B can read request A's user between assignment and use. Keep request state on `request` or in local variables. 2. **Import-time evaluation in URLconfs and modules.** On a recipe-sharing site, a `urls.py` that builds `path()` kwargs from `list(Category.objects.all())`, or a module-level `TODAY = date.today()`, is evaluated once when the module is imported. New categories never appear and "today" stops changing until workers restart. Worse, a database query at import time can run before the database is reachable, or during `manage.py` commands that never serve a request. 3. **Module-level caches and counters.** A global dict used as a cache or a request counter lives per process. With four workers there are four diverging copies, and none is cleared by a deploy until the process is replaced. 4. **Assuming a settings change is live.** Settings are read once; editing environment variables for a running process changes nothing until it restarts. 5. **Class attributes on class-based views.** The instance is fresh per request, but a mutable **class** attribute (a list defined on the class body) is shared by all instances in the process, so appending to it leaks across requests. ## How to reason about it - Ask of every value: **is this computed at import, at setup, or inside the request?** - Push data that can change into the view body or a callable evaluated per request. - Keep middleware `__init__` for configuration only, such as reading a setting or raising `MiddlewareNotUsed` to drop itself from the chain. - Use a shared store — the cache framework or the database — for anything every worker must agree on. ## What a senior answer shows The senior candidate names the one-time objects precisely (setup, handler, middleware instances, cached resolver), explains the lazy import of the URLconf, and connects each to a concrete incident: cross-request leakage through middleware attributes, stale import-time values, and per-worker divergence.

  • Why do these bugs rarely show up in tests or runserver?
    Tests usually send one request at a time through one process, so shared state is overwritten before anyone else reads it, and import-time values are fresh because the process just started. The bugs need concurrency within one process, several processes, or a long-lived process whose data changed after import.
  • When is the root URLconf module actually imported?
    The resolver is created and cached on first use, and its `urlconf_module` is imported lazily when the patterns are first needed, usually by the first request's resolution or a `reverse()` call. System checks also import it when commands like `runserver` run them. After that it stays in the process until restart.

saying these in an interview costs you the question

  • Says middleware __init__ runs for every request
  • Stores the current user as an attribute on a middleware instance
  • Believes code at the top of urls.py runs per request
  • Expects a module-level cache to be shared across worker processes
  • Thinks a class-based view instance is reused across requests