Middleware

Django · Advanced · 12 min

Cross-cutting concerns like timing, headers, tenancy and request IDs belong in middleware, not every view.

Middleware is a chain of wrappers around your views. Each one is a factory taking get_response (the next layer) and returning a callable that handles a request:

def timing_middleware(get_response):
    # one-time setup when the server starts
    def middleware(request):
        # before the view
        response = get_response(request)
        # after the view
        return response
    return middleware

Classes work the same way with __init__(self, get_response) and __call__(self, request).

List middleware in settings.MIDDLEWARE. Order matters: requests pass through top to bottom, responses bottom to top, like layers of an onion. AuthenticationMiddleware needs SessionMiddleware above it, and SecurityMiddleware usually comes first.

A middleware can short-circuit by returning a response without calling get_response, for example a maintenance page or an IP block, in which case inner layers and the view never run.

Optional hooks on class-based middleware:

  • process_view(request, view_func, view_args, view_kwargs): after URL resolution, before the view
  • process_exception(request, exception): when the view raises; return a response to handle it
  • process_template_response(request, response): modify a TemplateResponse before it renders

Built-in middleware handles sessions, auth, CSRF, messages, clickjacking headers, GZip, locale and (in Django 6.0) Content Security Policy.

Middleware runs on every request, so keep it fast. Mark middleware async_capable and provide an async path if you serve async views under ASGI, to avoid sync/async switching overhead.

Example

# core/middleware.py
import logging, time, uuid

log = logging.getLogger(__name__)
class RequestIdMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response               # called once at startup
    def __call__(self, request):
        request.id = request.headers.get("X-Request-ID") or uuid.uuid4().hex
        start = time.perf_counter()
        response = self.get_response(request)          # inner middleware + view
        response["X-Request-ID"] = request.id
        log.info("%s %s %s %.1fms", request.id, request.path, response.status_code,
                 (time.perf_counter() - start) * 1000)
        return response
    def process_exception(self, request, exception):
        log.exception("request %s failed", request.id)
        return None                                    # let Django's handling continue
# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "core.middleware.RequestIdMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    # ... CommonMiddleware, CsrfViewMiddleware, AuthenticationMiddleware, ...
]

Remember

  • Middleware wraps get_response: code before runs on the request, after on the response
  • MIDDLEWARE order: top-down for requests, bottom-up for responses
  • Returning early short-circuits the inner layers and the view
  • process_view / process_exception hooks for finer control

Watch out for

  • Placing a middleware that uses request.user above AuthenticationMiddleware.

Check yourself

  1. Middleware A is listed above B in MIDDLEWARE. In which order do they see the response?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/http/middleware/