Filter concepts by levelShowing all levels.

Django · Section 40

Middleware

Level
intermediate
Read
26 min
Concepts
3

Middleware wraps the entire request/response chain in one onion-layered pass — code before get_response(request) runs on the way in, code after runs on the way out, and MIDDLEWARE's order is the request-processing order going in, reversed for response processing coming out. Short-circuiting (returning an HttpResponse instead of calling get_response()) is a sanctioned way to reject a request before it reaches the view or any later middleware — used for maintenance-mode checks, rate limits, or early rejection. A custom middleware needs only __init__(get_response)/__call__(request); process_exception() reacts specifically to an unhandled view exception, async_capable declares async support (avoiding a per-layer sync/async adaptation cost under ASGI), and per-request state belongs on the request object itself, never on self, since a middleware instance is created once and shared across every request. The default middleware stack (SecurityMiddleware, SessionMiddleware, CommonMiddleware, CsrfViewMiddleware, AuthenticationMiddleware, MessageMiddleware, XFrameOptionsMiddleware) is a real dependency chain, not an arbitrary order — AuthenticationMiddleware and MessageMiddleware both need SessionMiddleware's session already loaded, which is why middleware order can affect correctness and security, exactly as the roadmap's own framing states.

This section
Django MiddlewareVery Academy

What is true here

  1. Middleware wraps the whole chain — MIDDLEWARE order is the request order going in, reversed for the response coming out.
  2. Short-circuiting (returning a response instead of calling get_response()) skips the view and every later middleware entirely.
  3. A middleware needs only __init__(get_response)/__call__(request); process_exception() reacts to an unhandled view exception specifically.
  4. Per-request state belongs on request, never on self — a middleware instance is created once and shared across every request.
  5. The default stack is a real dependency chain: AuthenticationMiddleware and MessageMiddleware both require SessionMiddleware to run first.

What you will be able to do

  • Reason correctly about what a middleware at a given position can and cannot see, on both the request and response side
  • Write a custom middleware that short-circuits appropriately and stores per-request state safely
  • Add async support to a middleware, and know when the sync/async adaptation cost actually matters
  • Reorder or remove a built-in middleware without breaking what depends on it

The request/response lifecycle

How middleware wraps the chain, why order matters both ways, and short-circuiting.

The request/response lifecycle, ordering, and short-circuiting

coreintermediate

Every middleware wraps the ENTIRE rest of the chain — its code before calling get_response(request) runs on the way IN (request processing), and its code after runs on the way OUT (response processing), for every single request, in the order MIDDLEWARE lists them going in and the REVERSE order coming back out. This is why MIDDLEWARE's order matters: SessionMiddleware must run before AuthenticationMiddleware (auth needs the session already loaded), and a middleware near the top of the list is also the last to see the response on the way out. Short-circuiting means a middleware can return an HttpResponse directly instead of calling get_response() — the request never reaches any middleware or view further down the chain, and the response starts unwinding back out immediately from that point.

Think of it as

The mental model Django's own docs use is "an onion" — request comes in through every layer from the outside in, hits the view at the very center, then the response passes back out through the same layers in reverse. This single-pass, wrap-everything shape is WHY ordering is not cosmetic: a middleware can only see what happened in layers CLOSER to the center (later in MIDDLEWARE) on the way in, because those haven't run yet when an earlier middleware's pre-get_response() code executes — and conversely, a middleware can only modify a response after every layer closer to the center has already added its own contribution, because response processing happens in reverse order, outermost last. Short-circuiting exists as a documented escape hatch specifically for "this request should never even reach the view" cases (an unauthenticated request to an admin-only path, a maintenance-mode check, a rate limit) — returning a response directly bypasses every remaining middleware AND the view entirely, which is a deliberate, sanctioned way to fail fast rather than something to work around.

python
class MyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # request processing
        response = self.get_response(request)
        # response processing
        return response

What we're doing: A middleware that short-circuits with a 503 during a maintenance window, so no request reaches the view or any later middleware.

core/middleware.pypython
class MaintenanceModeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if settings.MAINTENANCE_MODE and not request.path.startswith("/admin/"):
            return HttpResponse("Down for maintenance.", status=503)
        return self.get_response(request)
6
Returning directly here means get_response() is never called — the view, and any middleware listed after this one, never run for this request.
7
Placing this middleware EARLY in MIDDLEWARE matters — if it ran late, every earlier middleware (session, auth) would still do its work for a request that's about to be rejected anyway, wasted effort.

Why this works: Short-circuiting is the correct tool here specifically because "reject before doing any real work" is the goal — placing this check as early as reasonably possible in MIDDLEWARE, and returning directly rather than calling get_response(), avoids running authentication, session loading, or the view for a request that's going to be rejected regardless.

Placing a middleware that depends on request.session or request.user before SessionMiddleware/AuthenticationMiddleware in MIDDLEWARE

Wrong

python
MIDDLEWARE = [
    "core.middleware.LogUserActivityMiddleware",   # reads request.user
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",   # sets request.user
]

Better

python
MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "core.middleware.LogUserActivityMiddleware",   # now after both

What you see: AttributeError, or request.user silently missing/incorrect inside the custom middleware's request-processing code — request.user does not exist yet at that point in the chain, because AuthenticationMiddleware (which sets it) has not run yet for a middleware positioned before it.

Why: A middleware's request-processing code only sees what layers ABOVE it in MIDDLEWARE have already done — since request.user is specifically set by AuthenticationMiddleware's own request-processing step, any middleware placed before it in the list runs before that assignment happens at all, not just before some unrelated step.

The onion — request in, response out, in reverse

SecurityMiddleware

first in, last out

SessionMiddleware

must precede AuthenticationMiddleware

AuthenticationMiddleware

sets request.user

view

the center — last in, first out

  1. SecurityMiddleware — first in, last out
  2. SessionMiddleware — must precede AuthenticationMiddleware
  3. AuthenticationMiddleware — sets request.user
  4. view — the center — last in, first out

Request vs response processing, by position in MIDDLEWARE

Request vs response processing, by position in MIDDLEWARE
PositionSees requestSees response
First in MIDDLEWAREfirst (outermost, before anything else runs)last (after every other middleware has already processed it)
Last in MIDDLEWARElast (closest to the view)first (closest to where the view returned)

Together

python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
]

Remember: Middleware wraps the whole chain in one onion-layered pass — MIDDLEWARE's order is the request-processing order going in, and the REVERSE order for response processing coming out. SessionMiddleware must precede AuthenticationMiddleware. Short-circuiting (returning a response instead of calling get_response()) is a sanctioned way to reject a request before it reaches the view or any later middleware.

See also: custom and async middleware · the built in middleware stack · login logout and authentication backends

Advertisement

Custom and async middleware

Writing one, exception handling, async support, and where per-request state belongs.

Writing custom middleware, exception handling, and async support

coreadvanced

A custom middleware is a callable — __init__(self, get_response) stores the next layer, __call__(self, request) runs it. process_exception(self, request, exception) is an optional extra method Django calls specifically when a view raises an unhandled exception, letting middleware react (log it, return a custom error response) without needing a try/except around get_response() itself. Async middleware supports async def __call__ (or a sync one Django auto-wraps) so it can run inside an ASGI deployment without forcing a sync/async context switch for every single middleware layer — marked via async_capable = True (and sync_capable if it also supports WSGI). State management means per-request data a middleware wants to make available to the view/later middleware — set it as an attribute on request itself (e.g. request.tenant = ...), since request is the one object that survives the whole chain.

Think of it as

A custom middleware is deliberately just "a callable wrapping a callable" — no base class is required, which is why both a class with __call__ and a plain closure-returning function work identically; Django only cares that the thing passed to MIDDLEWARE, when instantiated with get_response, returns something callable with request. process_exception() exists as a separate hook (rather than requiring every middleware author to wrap get_response() in their own try/except) because exception handling has a genuinely different shape than normal response processing — it needs to run for EVERY middleware's process_exception in a defined order regardless of which layer's try/except would have caught it first, and Django's own exception-handling machinery (converting exceptions to responses via its handler machinery) already needs to happen somewhere central. Async support exists because Django is capable of running fully async under ASGI, but a single sync middleware in the chain would otherwise force an expensive thread-switch for every request — async_capable/sync_capable flags let Django adapt each middleware to the run mode it actually supports, sync-to-async or async-to-sync as needed, rather than requiring every middleware in a project to be rewritten together. Attaching custom data to request itself (not a global, not a thread-local) is the state-management convention because request is the one object every subsequent middleware and the view itself already receives as a parameter — it is the correct, request-scoped place for anything computed once per request and needed later in the same request's handling.

python
class MyMiddleware:
    async_capable = True
    sync_capable = True

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

    async def __call__(self, request):
        response = await self.get_response(request)
        return response

What we're doing: A middleware attaching a per-request correlation id, with a process_exception hook that logs it alongside any unhandled error.

core/middleware.pypython
class RequestIdMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request.request_id = uuid.uuid4().hex
        response = self.get_response(request)
        response["X-Request-Id"] = request.request_id
        return response

    def process_exception(self, request, exception):
        logger.error("Unhandled exception [%s]: %s", getattr(request, "request_id", "?"), exception)
        return None
6
Attached to request, not a module-level variable — safe under concurrent requests, since each request gets its own request object.
12
Returning None from process_exception() explicitly means "don't handle this response, let Django's normal exception machinery continue" — only return an HttpResponse to actually replace the default error response.

Why this works: A correlation id needs to be visible both to whatever the view does (it can read request.request_id to include in its own logs) and to error handling (process_exception can reference the same id) — request is the only object both code paths receive, making it the correct place to store this rather than, say, a module-level variable that would leak across concurrent requests.

Storing per-request state in a module-level variable or an instance attribute on the middleware itself instead of on request

Wrong

python
class TenantMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.current_tenant = None   # ONE middleware instance, shared across ALL requests

    def __call__(self, request):
        self.current_tenant = resolve_tenant(request)   # race condition under concurrency
        return self.get_response(request)

Better

python
class TenantMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request.tenant = resolve_tenant(request)   # scoped to THIS request object
        return self.get_response(request)

What you see: Under any concurrent request handling (multiple threads, async, multiple workers sharing state some other way), one request can see another's tenant/user/correlation data — a middleware instance is created ONCE and reused for every request, so an attribute on self is effectively global, shared, mutable state.

Why: Django creates a middleware instance once (typically at process/worker startup) and calls the same instance's __call__ for every subsequent request — self on a middleware is process-lifetime state, not request-lifetime state. request, by contrast, is a genuinely new object per request, which is exactly why it's the documented place for anything that must not leak between concurrent requests.

Where different middleware logic belongs

__init__(self, get_response)

one-time setup — runs once at startup

__call__(self, request)

per-request logic

process_exception(...)

reacts to an unhandled view exception

request.<attr> = ...

per-request data for later middleware/the view — never self

  1. __init__(self, get_response) — one-time setup — runs once at startup
  2. __call__(self, request) — per-request logic
  3. process_exception(...) — reacts to an unhandled view exception
  4. request.<attr> = ... — per-request data for later middleware/the view — never self

Where different middleware logic belongs

Where different middleware logic belongs
ConcernGoes in
One-time setup (config, connections)__init__(self, get_response)
Per-request logic__call__(self, request)
Reacting to an unhandled view exceptionprocess_exception(self, request, exception)
Per-request data for later middleware/the viewan attribute on request itself

Together

python
class TenantMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request.tenant = resolve_tenant(request)   # state, attached to request
        return self.get_response(request)

    def process_exception(self, request, exception):
        logger.exception("Unhandled error for tenant %s", getattr(request, "tenant", None))
        return None   # None means: let Django's normal exception handling continue

Remember: A middleware needs only __init__(get_response)/__call__(request) — no base class required. process_exception() reacts to an unhandled view exception specifically; return None to let Django's default handling continue. async_capable declares async support, avoiding a per-layer sync/async adaptation cost under ASGI. Per-request state belongs as an attribute on request, never on self (a middleware instance is shared across every request) or a module-level variable.

See also: the request response lifecycle · the built in middleware stack

Advertisement

The built-in middleware stack

Authentication, session, security, CSRF, and message middleware — and their dependency order.

The built-in middleware stack, and why its default order matters

standardintermediate

A new Django project's default MIDDLEWARE already orders SecurityMiddleware (HTTPS/HSTS-related headers, first, closest to raw request handling), SessionMiddleware (loads request.session), CommonMiddleware, CsrfViewMiddleware (checks CSRF tokens on unsafe methods), AuthenticationMiddleware (sets request.user, needs the session already loaded), MessageMiddleware (the messages framework, needs the session for storage), and XFrameOptionsMiddleware (clickjacking protection). Each depends on state an earlier middleware provides — AuthenticationMiddleware needs SessionMiddleware's session, MessageMiddleware's default storage needs the session too — which is why removing or reordering one is rarely safe without understanding exactly what it depends on.

Think of it as

The default MIDDLEWARE list is not an arbitrary convenience ordering — it is a real dependency chain, and Django ships it pre-ordered specifically so a new project doesn't have to work out these dependencies from scratch. SecurityMiddleware runs first/closest-to-raw-request because its concerns (redirect to HTTPS, HSTS headers) should apply before almost anything else even considers processing the request. SessionMiddleware precedes both AuthenticationMiddleware and MessageMiddleware because both of those depend on request.session already existing — authentication needs it to know who's logged in, and the default message storage backend persists messages IN the session. CsrfViewMiddleware's position (its actual check happens at the VIEW level via process_view, not simple request/response wrapping) means it runs its real check after routing has resolved a view but still needs to be listed early enough to see the request on the way in. Understanding this as a dependency graph, not an arbitrary list, is what makes "can I safely remove/reorder X" answerable — the question is always "what does X read that an earlier middleware sets, and what reads what X sets."

python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    ...
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]

What we're doing: Add a custom middleware that logs the authenticated user, positioned correctly relative to the built-in stack it depends on.

settings.pypython
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "core.middleware.LogAuthenticatedUserMiddleware",   # after AuthenticationMiddleware
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
6
Placed immediately after AuthenticationMiddleware specifically because it reads request.user — any position before that would see an unset or incomplete request.user.

Why this works: A custom middleware that depends on built-in middleware's state should be positioned relative to that dependency deliberately, not just appended to the end of the list — appending to the end happens to work here since request.user is already set by that point, but the reasoning ("after AuthenticationMiddleware, because it reads request.user") is what should drive the placement, not habit.

Removing SessionMiddleware to "simplify" a stateless API-only project, without checking what else depends on it

Wrong

python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    # SessionMiddleware removed — "we don't use sessions, this is an API"
    "django.contrib.auth.middleware.AuthenticationMiddleware",   # now breaks
]

Better

python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",   # kept — AuthenticationMiddleware needs it
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]
# if truly no session-based auth is needed anywhere, AuthenticationMiddleware
# should be removed too, or replaced with a token-based scheme that doesn't
# depend on request.session at all

What you see: AttributeError or a confusing failure inside AuthenticationMiddleware (or MessageMiddleware, if still present) — both read request.session, which no longer exists once SessionMiddleware is removed from the chain.

Why: SessionMiddleware looks removable in an API-only project since the API itself may not use sessions directly — but the built-in stack's later entries were written assuming it's present. Removing a dependency requires removing (or replacing) everything downstream that relies on it too, not just the one middleware that seemed unnecessary in isolation.

Default middleware, in order, and what each depends on

Default middleware, in order, and what each depends on
MiddlewareDepends on
SecurityMiddlewarenothing — runs first by design
SessionMiddlewarenothing — but many later middleware depend on IT
CommonMiddlewarenothing load-bearing from earlier middleware
CsrfViewMiddlewareURL resolution having found a view (checked via process_view)
AuthenticationMiddlewareSessionMiddleware (reads request.session)
MessageMiddlewareSessionMiddleware (default storage is session-backed)
XFrameOptionsMiddlewarenothing load-bearing — a response-header concern

Together

python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

Remember: The default MIDDLEWARE order is a real dependency chain, not arbitrary: SessionMiddleware before AuthenticationMiddleware (needs the session) and MessageMiddleware (default storage is session-backed); SecurityMiddleware first for HTTPS/HSTS concerns before anything else runs. CsrfViewMiddleware's real check happens in process_view(), after URL resolution, not plain request processing. Removing a built-in middleware means checking everything downstream that depends on it too.

See also: the request response lifecycle · session storage backends · login logout and authentication backends

Advertisement