Understanding Django Middleware Architecture and How to Build Custom Middleware

This article explains Django’s middleware system, how middleware components interact with the request/response cycle, how to activate and order middleware, how to write custom middleware using functions or classes, how to use special middleware hooks such as process_view, process_exception, and process_template_response, and how to properly handle streaming responses.

Django middlewareprocess_viewrequest/response cycle

~7 min read • Updated Mar 14, 2026

Introduction

Middleware in Django is a lightweight, low-level plugin system that allows developers to globally modify or process incoming requests and outgoing responses. Each middleware component performs a specific task. For example, AuthenticationMiddleware associates users with requests using sessions.


This article explains how middleware works, how to activate it, and how to write custom middleware components.


Writing Custom Middleware

A middleware component is a callable that receives a request and returns a response. Middleware can be implemented as a function or as a class.


Function-Based Middleware

def simple_middleware(get_response):
    # One-time configuration and initialization.

    def middleware(request):
        # Code executed before the view (and later middleware).
        response = get_response(request)
        # Code executed after the view.
        return response

    return middleware

Class-Based Middleware

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration.

    def __call__(self, request):
        # Code executed before the view.
        response = self.get_response(request)
        # Code executed after the view.
        return response

The get_response callable may be the next middleware in the chain or a wrapper around the view. Middleware does not need to know what comes next.


Notes on __init__

  • It must accept only the get_response argument.
  • It runs once when the server starts, not per request.

Marking Middleware as Unused

If __init__ raises MiddlewareNotUsed, Django removes the middleware from the processing chain.


Activating Middleware

To activate middleware, add its full Python path to the MIDDLEWARE list in Django settings:


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",
]

The order of middleware is important because some middleware depend on others. For example, AuthenticationMiddleware must run after SessionMiddleware.


Middleware Order and Layering

During the request phase, middleware is applied top-down. During the response phase, middleware is applied bottom-up.


This structure behaves like an onion: each middleware layer wraps the next. If a middleware returns a response without calling get_response, inner layers and the view will not execute.


Additional Middleware Hooks

Class-based middleware can define three optional methods for more control.


process_view()

Called just before Django calls the view.

  • request: The request object.
  • view_func: The view function.
  • view_args: Positional arguments for the view.
  • view_kwargs: Keyword arguments for the view.

If it returns None, Django continues processing. If it returns an HttpResponse, the view is skipped.


Note: Accessing request.POST here prevents later modification of upload handlers.


process_exception()

Called when the view raises an exception.


If it returns an HttpResponse, Django uses it. Otherwise, default exception handling applies.


process_template_response()

Called after the view returns a TemplateResponse or similar object.


It must return a response object with a render() method. It may modify the response or return a new one.


Handling Streaming Responses

StreamingHttpResponse does not have a content attribute. Middleware must check whether a response is streaming:


if response.streaming:
    response.streaming_content = wrap_streaming_content(response.streaming_content)
else:
    response.content = alter_content(response.content)

Streaming content may be too large to load into memory. Middleware must wrap it, not consume it:


def wrap_streaming_content(content):
    for chunk in content:
        yield alter_content(chunk)

Conclusion

Django’s middleware system provides powerful hooks into the request/response cycle. By understanding middleware ordering, lifecycle, and special hooks, developers can build custom middleware that enhances security, performance, and functionality across an entire Django application.


Streaming Response Handling

StreamingHttpResponse supports both synchronous and asynchronous iterators. Middleware that wraps streaming content must match the iterator type. If your middleware needs to support both, check StreamingHttpResponse.is_async to determine the correct behavior.


Exception Handling in Middleware

Django automatically converts exceptions raised by views or middleware into appropriate HTTP responses. Known exceptions become 4xx responses, while unknown exceptions become 500 errors.


This conversion happens before and after each middleware layer, acting like a thin film between layers of the “onion.” As a result:

  • Middleware always receives an HttpResponse from get_response, never an exception.
  • Even if the next middleware raises Http404, your middleware receives a response with status_code = 404.

To disable this conversion and allow exceptions to propagate, set DEBUG_PROPAGATE_EXCEPTIONS = True.


Asynchronous Support in Middleware

Middleware can support synchronous requests, asynchronous requests, or both. Django adapts requests to match middleware capabilities, but this may reduce performance.


Capability Flags

Set these attributes on your middleware factory or class:

  • sync_capable: Defaults to True.
  • async_capable: Defaults to False.

If both are True, Django passes the request without adaptation. You can detect async mode by checking whether get_response is a coroutine using iscoroutinefunction.


Decorators for Middleware Capabilities

Django provides decorators:

  • sync_only_middleware()
  • async_only_middleware()
  • sync_and_async_middleware()

Hybrid Middleware Example

from asgiref.sync import iscoroutinefunction
from django.utils.decorators import sync_and_async_middleware

@sync_and_async_middleware
def simple_middleware(get_response):
    if iscoroutinefunction(get_response):

        async def middleware(request):
            response = await get_response(request)
            return response

    else:

        def middleware(request):
            response = get_response(request)
            return response

    return middleware

Note: Even if the view is async, Django may call your middleware in sync mode if synchronous middleware exists between you and the view.


Asynchronous Class-Based Middleware

from asgiref.sync import iscoroutinefunction, markcoroutinefunction

class AsyncMiddleware:
    async_capable = True
    sync_capable = False

    def __init__(self, get_response):
        self.get_response = get_response
        if iscoroutinefunction(self.get_response):
            markcoroutinefunction(self)

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

Upgrading Legacy Middleware

Django provides MiddlewareMixin to help upgrade pre-Django 1.10 middleware to the modern system.


What MiddlewareMixin Provides

  • An __init__() that stores get_response.
  • A __call__() method that:
Calls process_request()
Calls get_response()
Calls process_response()

Under MIDDLEWARE_CLASSES, __call__() is ignored and Django calls process_request and process_response directly.


Behavioral Differences Between MIDDLEWARE and MIDDLEWARE_CLASSES

1. Response Flow

Under MIDDLEWARE, middleware behaves like an onion: only layers that saw the request will see the response.


Under MIDDLEWARE_CLASSES, all process_response methods run, even if earlier middleware short-circuited.


2. Exception Handling

Under MIDDLEWARE, process_exception only handles exceptions from the view.


Under MIDDLEWARE_CLASSES, it also handles exceptions from process_request.


3. Exceptions in process_response

Under MIDDLEWARE_CLASSES, exceptions in process_response skip earlier middleware and always return a 500 error.


Under MIDDLEWARE, exceptions are converted to HTTP responses and passed to the next middleware.


Conclusion

Django’s middleware system provides powerful hooks for customizing request and response processing. Understanding streaming behavior, exception handling, async support, and legacy compatibility enables developers to build robust, efficient, and modern middleware components tailored to their application’s needs.


Written & researched by Dr. Shahin Siami