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.
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.
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.
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.
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.
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