Django Middleware Explained: How It Works

Blog / Django · September 7, 2019 · Updated June 10, 2026 · 8 min read
Django Middleware Explained: How It Works

Django middleware is a lightweight plugin layer that sits between the web server and your views, processing every HTTP request on the way in and every response on the way out. Each middleware is a callable that receives a get_response function and returns a function that takes a request and returns a response — letting you modify requests, responses, authentication, security headers, sessions, and errors globally without editing individual views.

This guide is rewritten for Django 5.x (Django 5.2 is the current LTS in 2026). It covers the modern MIDDLEWARE setting, the new-style factory pattern, ordering rules, the optional hook methods, async-capable middleware, and how to migrate legacy MIDDLEWARE_CLASSES code.

Key takeaways

  • Middleware processes requests top-to-bottom and responses bottom-to-top through the MIDDLEWARE list.
  • Modern middleware is a factory: def simple_middleware(get_response) returning an inner middleware(request) callable — no base class required.
  • The pre-1.10 MIDDLEWARE_CLASSES setting (with process_request/process_response) was removed in Django 2.0; current projects use MIDDLEWARE.
  • Optional hooks — process_view, process_exception, and process_template_response — give you finer control alongside __call__.
  • Middleware can be async-capable using the sync_capable/async_capable flags plus markcoroutinefunction/iscoroutinefunction.
  • Order matters: SecurityMiddleware belongs first, and SessionMiddleware must come before AuthenticationMiddleware.

What is middleware in Django?

Middleware is a framework of hooks into Django's request/response processing. It is a chain of callables wrapped around your view: a request passes down through every enabled middleware before it reaches the view, and the view's response passes back up through the same chain in reverse before it is returned to the browser.

Because middleware runs for every request, it is the right home for cross-cutting concerns that shouldn't be duplicated in each view — security headers, session loading, authentication, CSRF protection, locale selection, GZip compression, logging, and custom request enrichment. For behaviour that only applies to certain views, you usually want a custom decorator to check user roles and permissions instead, which keeps the logic local to the views that need it.

How does the middleware request/response cycle work?

Think of the MIDDLEWARE list as an onion wrapped around the view. On the way in, Django calls each middleware from the top of the list downward; on the way out, the response travels back up from the bottom to the top. The same middleware therefore sees the request before the view runs and the response after it.

In a factory-style middleware, the code you write before calling get_response(request) is the request phase, and the code after it is the response phase:

import time


def simple_middleware(get_response):
    # One-time configuration and initialisation (runs once at startup).

    def middleware(request):
        # --- Request phase: runs on the way IN, before the view. ---
        start = time.monotonic()

        response = get_response(request)

        # --- Response phase: runs on the way OUT, after the view. ---
        response["X-Response-Time"] = f"{time.monotonic() - start:.3f}s"
        return response

    return middleware

Here get_response is either the next middleware in the chain or, for the innermost middleware, the view itself. Calling it advances the request one step deeper into the onion; its return value is the response bubbling back out. A middleware can also short-circuit the chain by returning a response without calling get_response — that's how CsrfViewMiddleware can reject a forged request before it ever reaches your view.

How do you write custom middleware?

You can write middleware as a factory function (above) or as a class. The class form is handy when you want to keep state on the instance or implement the optional hook methods. A class needs __init__(self, get_response) and __call__(self, request), and may add hooks such as process_exception:

import logging
import time

logger = logging.getLogger("requests")


class TimingMiddleware:
    def __init__(self, get_response):
        # Runs once when the web server starts.
        self.get_response = get_response

    def __call__(self, request):
        # Request phase (on the way in, top-to-bottom).
        request.start_time = time.monotonic()

        response = self.get_response(request)

        # Response phase (on the way out, bottom-to-top).
        duration = time.monotonic() - request.start_time
        response["X-Response-Time"] = f"{duration:.3f}s"
        return response

    def process_exception(self, request, exception):
        # Called only if a view raises an uncaught exception.
        logger.error("View error on %s: %s", request.path, exception)
        return None  # None lets Django's normal exception handling continue

Middleware hook methods

Beyond __call__, a middleware class can implement optional hooks that Django calls at specific points. These provide the granular control the old MIDDLEWARE_CLASSES API offered, but on top of the new-style callable:

Hook When it runs Return value
__call__(self, request) Wraps the whole request (required) The HttpResponse
process_view(request, view_func, view_args, view_kwargs) Just before Django calls the view None to continue, or an HttpResponse to short-circuit
process_exception(request, exception) When a view raises an uncaught exception None to fall through, or an HttpResponse
process_template_response(request, response) After the view, if the response has a render() method A response object (must return one)

For full control over what users see when something goes wrong, pair process_exception with custom error pages in Django.

What's the order of MIDDLEWARE?

Order is significant because each middleware depends on what runs before it. For example, AuthenticationMiddleware needs the session loaded first, so SessionMiddleware must appear earlier in the list. To activate your own middleware, add its dotted import path to the MIDDLEWARE list at the position that matches what it does:

# settings.py
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",
    # Your custom middleware, placed after auth so request.user is available:
    "my_app.middleware.TimingMiddleware",
]

Default Django MIDDLEWARE and what each does

A fresh Django 5.x project ships with this default stack:

Middleware Responsibility
SecurityMiddleware SSL redirects, HSTS, and other security headers; goes first
SessionMiddleware Loads and saves the session across requests
CommonMiddleware URL rewriting (APPEND_SLASH), DISALLOWED_USER_AGENTS, ETags
CsrfViewMiddleware Adds and validates CSRF tokens to block cross-site request forgery
AuthenticationMiddleware Attaches request.user based on the session
MessageMiddleware Enables the cookie/session-based messages framework
XFrameOptionsMiddleware Sets X-Frame-Options to protect against clickjacking

Because request processing runs top-to-bottom, SecurityMiddleware sees requests first; because response processing runs bottom-to-top, SecurityMiddleware also gets the last word on outgoing headers.

How do you migrate legacy MIDDLEWARE_CLASSES code?

Before Django 1.10 there were five separate hooks — process_request, process_view, process_exception, process_template_response, and process_response — and middleware was registered under MIDDLEWARE_CLASSES. The first two ran before the view and the last three after it. Django 2.0 removed MIDDLEWARE_CLASSES entirely, so every project must use the MIDDLEWARE setting and the factory style.

Here is the difference at a glance:

Aspect Old-style (pre-1.10, removed) New-style (Django 1.10+, current)
Setting MIDDLEWARE_CLASSES MIDDLEWARE
Shape Class with process_request/process_response Callable factory taking get_response
Base class Plain class None required (or MiddlewareMixin)
Phasing Separate request/response methods Code before/after get_response()
Async support No Yes (async_capable)

If you have old middleware you can't immediately rewrite, wrap it with MiddlewareMixin, which adapts the old process_request/process_response methods to the new callable interface:

from django.utils.deprecation import MiddlewareMixin


class CustomMiddleware(MiddlewareMixin):
    def process_request(self, request):
        # Runs before the view (legacy hook, adapted by MiddlewareMixin).
        pass

    def process_response(self, request, response):
        # Runs after the view; must return the response.
        return response

Does Django middleware support async?

Yes. Since Django 3.1, middleware can run under ASGI without blocking the event loop, and that support is mature in Django 5.x. Django inspects two class attributes — sync_capable and async_capable — to decide how to call your middleware, and it adapts (wrapping in a thread or an async shim) when a middleware supports only one mode. MiddlewareMixin sets both to True.

For a hand-written class, declare the flags and use markcoroutinefunction/iscoroutinefunction from asgiref.sync so Django can detect and call the async path. (For function-style middleware, the @sync_and_async_middleware decorator from django.utils.decorators does the equivalent.)

from asgiref.sync import iscoroutinefunction, markcoroutinefunction


class AsyncCapableMiddleware:
    # Tell Django this middleware works in both modes.
    sync_capable = True
    async_capable = True

    def __init__(self, get_response):
        self.get_response = get_response
        # If the chain below us is async, mark this instance as a coroutine.
        if iscoroutinefunction(self.get_response):
            markcoroutinefunction(self)

    def __call__(self, request):
        if iscoroutinefunction(self.get_response):
            return self.__acall__(request)
        # Synchronous path.
        response = self.get_response(request)
        return response

    async def __acall__(self, request):
        # Asynchronous path.
        response = await self.get_response(request)
        return response

Putting it together

Middleware is one of Django's most powerful extension points: a small, ordered chain that lets you apply security, logging, and request enrichment across an entire app from one place. Reach for middleware when behaviour must apply to every request; reach for narrower tools — Django signals, custom management commands, or permissions and groups — when the concern is local to a model, a command, or a view.

Building or modernising a Django application and want middleware, security, and architecture done right? Our team offers Django development services for startups and enterprises, shipping production features in days to weeks. We've delivered 50+ projects over 12+ years across the USA, UK, Australia, Canada, Singapore, and beyond.

Frequently Asked Questions

What is middleware in Django?

Django middleware is a plugin layer that processes every HTTP request before it reaches a view and every response before it returns to the client. Each middleware is a callable that takes a get_response function and returns a function accepting a request and returning a response, making it ideal for global concerns such as authentication, security headers, sessions, and logging.

How do I write custom middleware in Django 5.x?

Write a factory function def my_middleware(get_response): that defines an inner def middleware(request):, runs your request-phase code, calls response = get_response(request), runs your response-phase code, and returns the response. Alternatively use a class with __init__(self, get_response) and __call__(self, request). Then add the dotted import path to the MIDDLEWARE list in settings.py.

What is the difference between MIDDLEWARE and MIDDLEWARE_CLASSES?

MIDDLEWARE_CLASSES was the pre-1.10 setting where each middleware was a class implementing process_request, process_response, and related hooks. MIDDLEWARE, introduced in Django 1.10, uses the new-style factory pattern that takes get_response. Django 2.0 removed MIDDLEWARE_CLASSES completely, so all current projects, including Django 5.x, must use MIDDLEWARE.

In what order does Django run middleware?

Django processes the MIDDLEWARE list top-to-bottom for incoming requests and bottom-to-top for outgoing responses, so the first middleware listed sees the request first and the response last. Order matters: SessionMiddleware must precede AuthenticationMiddleware, and SecurityMiddleware is placed first so it handles requests before anything else.

What are process_view, process_exception, and process_template_response?

They are optional hook methods a middleware class can implement. process_view runs just before the view is called, process_exception runs when a view raises an uncaught exception, and process_template_response runs after the view if the response has a render() method. Each can return None to continue or an HttpResponse to short-circuit, except process_template_response, which must return a response object.

Does Django middleware support async views?

Yes. Since Django 3.1, middleware can be async-capable. Declare the sync_capable and async_capable class attributes and use markcoroutinefunction/iscoroutinefunction from asgiref.sync so Django calls the correct path. Django automatically adapts middleware that supports only one mode, but mixing forces conversions, so matching your middleware to your stack (ASGI vs WSGI) gives the best performance.

Share this article