Django

Middleware and the request/response cycle

Every request passes through a chain of middleware on the way in and back out again — understanding the order explains a whole class of "why did my code run twice" bugs.

Advanced

3 min read

The chain, not a list of independent hooks

MIDDLEWARE in settings.py isn't a set of independent plugins each doing their own thing to the request — it's an onion. Each middleware wraps the one below it, so the request passes through the list top-to-bottom on the way in, and back through it bottom-to-top on the way out:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",   # 1st in, last out
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]

For a single request: SecurityMiddleware runs first, calls into SessionMiddleware, which calls into CommonMiddleware, and so on, until the innermost call reaches the view. The view's return value (the response) then travels back out through the same chain in reverse — AuthenticationMiddleware sees it first, SecurityMiddleware sees it last, right before it's sent to the client. This is exactly why order in the list matters: AuthenticationMiddleware has to run after SessionMiddleware, because it reads request.session to look up the logged-in user — if the order were reversed, request.user wouldn't exist yet when AuthenticationMiddleware tried to set it.

Writing one, mechanically

class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response   # the next layer in, called once at startup
 
    def __call__(self, request):
        start = time.monotonic()
 
        # --- everything here runs on the way IN, before the view ---
        response = self.get_response(request)   # this line calls the next middleware / the view
        # --- everything here runs on the way OUT, after the view ---
 
        elapsed = time.monotonic() - start
        response["X-Response-Time"] = f"{elapsed:.3f}s"
        return response

get_response is the callable representing "the rest of the chain" — calling it is what hands control to the next middleware (or, at the innermost layer, the view itself). Code placed before that call runs on every request, on the way in; code placed after it runs on every response, on the way out — which is exactly what makes middleware the right place for cross-cutting concerns like timing, logging, and adding response headers, instead of repeating the same logic in every individual view.

Short-circuiting the chain

A middleware doesn't have to call get_response at all. Returning an HttpResponse directly, without calling the next layer, stops the request from ever reaching the view — or any middleware further down the chain:

class MaintenanceModeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
 
    def __call__(self, request):
        if settings.MAINTENANCE_MODE:
            return HttpResponse("Down for maintenance", status=503)   # view never runs
        return self.get_response(request)

This is the mechanism behind things like CsrfViewMiddleware rejecting a request with a 403 before it ever reaches a view — the check happens in the "on the way in" half of the middleware, and returning early instead of calling get_response is what prevents the rest of the chain from running at all.

Why "why did my logging run twice" usually traces back here

A common real bug: a middleware placed too early in the list logs or mutates request before authentication middleware has attached request.user, so it always sees an anonymous user even for logged-in requests — not because the middleware is broken, but because of where it sits in the chain relative to AuthenticationMiddleware. Reading MIDDLEWARE top-to-bottom as "this order is the actual call order, both directions" is the fastest way to debug this class of issue, rather than treating the list as unordered configuration.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why must SessionMiddleware come before AuthenticationMiddleware in the MIDDLEWARE list?

2. In a middleware's __call__ method, what does code placed AFTER `response = self.get_response(request)` run during?

3. What happens if a middleware returns an HttpResponse directly instead of calling self.get_response(request)?

4. A middleware placed too early in the list reads request.user and always sees an anonymous user, even for logged-in requests. What's the likely cause?