A Simple Guide to Django Middleware

When you first start with Django things feel straightforward. A URL maps to a view. The view talks to a model and renders a template. But then you notice things happening that you didn't explicitly write in your view. How does user authentication work on every request? How does Django know to protect against cross site request forgery?

The answer is middleware. It’s a powerful idea that often feels like magic. But it’s not magic. It’s a simple and elegant system for hooking into Django’s request and response processing. Understanding it is key to building more complex applications.

What is Middleware?

Think of middleware as a series of layers. When a request comes into your application it passes down through each middleware layer before it reaches your view. After your view produces a response the response passes back up through those same layers in reverse order.

Each layer can inspect or modify the request. A layer can also inspect or modify the response. Some middleware might even decide to short circuit the whole process and return a response immediately without ever hitting the view.

This is incredibly useful for code that needs to run on every request or on many requests. We call these cross cutting concerns. Authentication security logging and session handling are all perfect examples. By putting this logic in middleware you keep your views focused on their specific job.

How Django Uses It

If you look in your project’s settings.py file you will find a MIDDLEWARE setting. It’s a list of strings.

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

This list is the stack of middleware layers for your application. The order here is very important. The request passes down this list from top to bottom. The response passes back up from bottom to top. For example the SessionMiddleware must run before AuthenticationMiddleware because the authentication system uses session data.

Writing Your Own Middleware

The best way to understand middleware is to write some. Let’s make a very simple one that measures how long each request takes to process.

Modern Django middleware is written as a callable class. It needs an __init__ method that takes get_response as an argument and a __call__ method that takes the request.

Create a new file in one of your apps say myapp/middleware.py.

import time

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

    def __call__(self, request):
        # Code to be executed for each request before
        # the view and later middleware are called.
        start_time = time.time()

        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.
        duration = time.time() - start_time
        print(f“Request to {request.path} took {duration:.2f} seconds”)

        return response

Let’s break this down. The __init__ method is called only once when the web server starts up. Django gives it the get_response callable. This callable represents the next layer in the middleware stack or the view itself if this is the last middleware. You just need to store it.

The __call__ method is the important part. It gets called for every single request. Here we grab the time before calling self.get_response(request). This passes the request on to the next layer and eventually gets a response object back. Once we have the response we calculate the duration and print it. Finally we must return the response so the process can continue.

To activate this middleware you just add it to your settings.py file. The path is the dotted Python path to the class.

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    # Add our new middleware near the top
    'myapp.middleware.TimingMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    # ... the rest of the list
]

Now run your development server and make a few requests. You will see the timing information printed in your console.

A More Practical Example

Printing to the console is useful for debugging but middleware can do much more. Let’s write middleware that enforces a simple rule. Maybe we want to build an API that requires a special header for access.

This middleware will check for an X-API-KEY header. If it’s missing or incorrect it will return a 403 Forbidden response immediately. This prevents the request from ever reaching the view.

from django.http import HttpResponseForbidden

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

    def __call__(self, request):
        # We only want to protect paths under /api/
        if request.path.startswith('/api/'):
            api_key = request.headers.get('X-Api-Key')
            if not api_key or api_key != 'my-secret-key':
                return HttpResponseForbidden('Invalid or missing API Key.')

        response = self.get_response(request)
        return response

This example shows how middleware can short circuit. If the API key is invalid we create and return an HttpResponseForbidden directly. The call to self.get_response is never reached. The request never makes it to the view. This is a very efficient way to handle authorization or validation logic that applies to a whole section of your site.

Again you would add 'myapp.middleware.ApiKeyMiddleware' to your settings.py list. Where you place it matters. You'd likely want it after session and auth middleware but before anything that does heavy processing. You want to fail fast.

The Power of Simplicity

Middleware is a simple pattern. It is just a chain of callables. But it gives you enormous power to add global behavior to your application without cluttering your views.

Good Django code often involves moving logic out of views. Middleware is one of the primary tools for this alongside service objects custom template tags and model managers.

When you find yourself writing the same line of code at the top of many different views it’s a signal. That code might belong in middleware. It could be for setting something on the request object handling a specific header or managing a connection to another service.

The built in middleware handles the most common cases but custom middleware is what lets you tailor the request response cycle to your application’s unique needs. It is a fundamental part of the Django framework and worth your time to master.

Think about a repetitive task in one of your Django views

— Rishi Banerjee
September 2025