How do you write a custom Django middleware, and which part of it runs once versus on every request?
answer
- a factory, not a plain function
- the next layer is passed in
- constructor versus call
- a dotted path in a list
basics
~10 sWrite a factory that receives get_response: a class whose init(self, get_response) runs once at startup and whose call(self, request) runs per request, calling get_response and returning the response. Register its dotted path in MIDDLEWARE.
solid answer
~40 sA Django middleware is a **factory**: a class (or function) that Django calls once, when the handler loads `MIDDLEWARE` at startup, passing `get_response`, the next layer of the chain. `__init__` may take only `get_response` and is the place for one-time setup; raising `django.core.exceptions.MiddlewareNotUsed` there removes the middleware from the chain. `__call__(request)` runs on every request: code before `self.get_response(request)` sees the request on the way in, code after it sees the response on the way out, and it must return a response. A request-timing middleware reads `time.perf_counter()` before the call, sets a `Server-Timing` header on the response after it, and returns it. You activate it by adding its dotted path, such as `"core.middleware.TimingMiddleware"`, to the `MIDDLEWARE` list, and its position there decides what it wraps.
code
python · 20 linesimport time
from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
class TimingMiddleware:
def __init__(self, get_response):
# Runs once, when Django builds the middleware chain.
if not getattr(settings, "REQUEST_TIMING_ENABLED", True):
raise MiddlewareNotUsed("request timing disabled")
self.get_response = get_response
def __call__(self, request):
# Runs on every request.
start = time.perf_counter()
response = self.get_response(request)
elapsed_ms = (time.perf_counter() - start) * 1000
response["Server-Timing"] = f"app;dur={elapsed_ms:.1f}"
return responsego deeper
Recall the two methods, init with get_response and call with request, that the response must be returned, and that the dotted path goes into MIDDLEWARE.
Explain that Django instantiates each entry once when it builds the chain, why that makes self unsuitable for request data, and what MiddlewareNotUsed does at startup.
Show judgement about what is safe to compute once, where a timing layer should sit to measure what you care about, and how a missing return or a shared attribute fails under load.
Decide which cross-cutting concerns deserve a project middleware at all, since every layer runs on every request, and which belong in views or decorators instead.
## The factory protocol In Django a **middleware factory** is a callable that takes one argument, `get_response`, and returns a **middleware**: a callable that takes a request and returns a response, exactly like a view. `get_response` is whatever comes next: the next middleware in the list, or, for the last one, a handler wrapper that resolves the URL and calls the view. A middleware does not need to know which. Most middleware is written as a class, because the class itself is the factory and its instances are the middleware: - the class is called with `get_response`, so `__init__(self, get_response)` runs; - the resulting instance is called with each request, so `__call__(self, request)` runs. A function-based factory works the same way: an outer function receives `get_response`, does its setup and returns an inner function that handles each request. ## Once versus per request | Part | When it runs | What belongs there | |---|---|---| | `__init__(self, get_response)` | once, when Django builds the chain at startup | store `get_response`, read settings, build expensive helpers, or raise `MiddlewareNotUsed` | | code before `self.get_response(request)` | on every request, on the way in | inspect or annotate the request, or return a response early | | code after it | on every request, on the way out | inspect or modify the response | The once-only `__init__` is the part people get wrong. Django's handler calls `load_middleware()` when it is created, which instantiates every entry in `MIDDLEWARE` a single time; after that, the same instance serves every request the process handles. Anything specific to one request therefore belongs in local variables inside `__call__` or on the `request` object, never on `self`. ## A request-timing middleware The classic first middleware measures how long the rest of the stack took: 1. `__init__` stores `get_response` (and, optionally, decides whether timing is enabled at all). 2. `__call__` records `time.perf_counter()` in a local variable. 3. It calls `self.get_response(request)`, which runs every layer below it and the view. 4. It computes the elapsed time and writes it into a response header, such as `Server-Timing: app;dur=12.3`. 5. It returns the response, so the layers above it, and finally the server, receive it. Forgetting step 5 is a common bug: the layer above gets `None` instead of a response and the request ends in an error. ## Registering it A middleware does nothing until its **dotted Python path** is added to the `MIDDLEWARE` setting. Django's own default for that setting is an empty list; `startproject` writes a list of seven built-ins into the generated `settings.py`. The order is meaningful: the first entry is the outermost layer, so it sees the request first and the response last. A timing middleware placed near the top measures almost the whole stack; one placed at the bottom measures little more than the view. The older `MIDDLEWARE_CLASSES` setting, with its separate `process_request`/`process_response` protocol, was removed in Django 2.0. Django's built-in middleware still defines those two methods through `django.utils.deprecation.MiddlewareMixin`, whose `__call__` invokes them around `get_response`, but new middleware can simply implement `__call__`. ## Switching it off at startup - Raising `MiddlewareNotUsed` from `__init__` tells Django to leave the middleware out of the chain entirely; when `DEBUG` is true, Django logs a debug message to the `django.request` logger. - Because the decision happens in `__init__`, it is made once per process, not per request. A per-request opt-out belongs in `__call__`, as an early branch that just calls `get_response`. - A factory that returns `None` instead of a middleware is a configuration error: Django raises `ImproperlyConfigured` naming the factory. ## Rules that keep it correct - `__init__` must accept exactly `get_response`; Django passes nothing else, so configuration comes from settings or class attributes. - Always return a response from `__call__`. - Keep per-request data out of `self`. - Reading `request.POST` before the view runs prevents the view from changing upload handlers, so avoid it in general-purpose middleware. - Not every response has `content`: a `StreamingHttpResponse` exposes `streaming_content` instead. Setting a header works on both.
- Why can a Django middleware's __init__ take only get_response?Django instantiates each `MIDDLEWARE` entry itself and passes exactly one argument, the next layer. Configuration therefore comes from settings or class attributes read inside `__init__`. Because `__init__` runs once per process, whatever it reads is fixed until the process restarts, which is right for configuration and wrong for anything per request.
- What happens if a middleware's __call__ forgets to return the response?The layer above receives `None` where it expects an `HttpResponse`. As soon as that layer or the handler touches the response, for example to set a header, it fails, and the request ends in an error, typically a 500. Returning the response on every path, including early branches, avoids it.
- When would you raise MiddlewareNotUsed instead of branching in __call__?When the decision is fixed for the life of the process, such as a feature disabled by a setting in this environment. Raising it in `__init__` removes the layer from the chain, so disabled middleware costs nothing per request. A decision that depends on the request, such as skipping health-check paths, belongs in `__call__`.
saying these in an interview costs you the question
- __init__ runs on every request, so it can hold per-request data.
- A middleware must subclass a Django base class to be accepted.
- Custom middleware is registered in MIDDLEWARE_CLASSES.
- Raising MiddlewareNotUsed skips the middleware for the current request only.
- Code after get_response runs before the view is called.