Python Django: How to Set Up Custom Middleware in Django
Middleware is one of Django's most powerful features, allowing you to process every request and response that flows through your application. It acts as a pipeline of hooks sitting between the client and your views, ideal for tasks like logging, authentication, header manipulation, and performance monitoring.
This guide explains how middleware works in Django and walks you through creating, registering, and testing a custom middleware from scratch.
Understanding How Middleware Works in Django
Middleware in Django is a lightweight, modular component that intercepts requests and responses globally. Every time a user sends a request to your Django application, the request passes through each middleware in order before reaching the view. After the view generates a response, the response travels back through the middleware stack in reverse order before being returned to the client.
Client → Middleware 1 → Middleware 2 → Middleware 3 → View
Client ← Middleware 1 ← Middleware 2 ← Middleware 3 ← View
Common use cases for middleware include:
- Request/response logging for debugging and monitoring
- Authentication and authorization checks
- Session management
- Security headers (CORS, HSTS, etc.)
- Performance measurement
- Content filtering or transformation
The Structure of a Custom Middleware Class
A Django middleware is a Python class that implements at minimum the __init__ and __call__ methods:
class MyCustomMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time initialization when the server starts
def __call__(self, request):
# Code executed BEFORE the view (request phase)
response = self.get_response(request)
# Code executed AFTER the view (response phase)
return response
| Method | When It Runs | Purpose |
|---|---|---|
__init__(self, get_response) | Once at server startup | Store get_response and perform one-time setup |
__call__(self, request) | On every request | Process the request before and/or after the view |
process_exception(self, request, exception) | When a view raises an exception | Handle or log uncaught exceptions |
process_template_response(self, request, response) | When the view returns a TemplateResponse | Modify the template response before rendering |
The get_response callable is what connects your middleware to the next middleware in the chain (or to the view itself if your middleware is last).
Building a Request Timing Middleware Step by Step
In this walkthrough, you will create a middleware that measures how long each request takes to process, i.e. a practical tool for performance monitoring.
Prerequisites
pip install django
django-admin startproject middleware_project
cd middleware_project
python manage.py startapp myapp
Add myapp to INSTALLED_APPS in settings.py:
# middleware_project/settings.py
INSTALLED_APPS = [
# ... default apps ...
'myapp',
]
Step 1: Create the Middleware File
Create the directory structure myapp/middleware/ with an __init__.py file, then add your middleware module:
mkdir myapp/middleware
touch myapp/middleware/__init__.py
Now create myapp/middleware/request_time_logging.py:
import time
import logging
logger = logging.getLogger(__name__)
class RequestTimeLoggingMiddleware:
"""Middleware that logs the processing time of each request."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Record the start time before the view processes the request
start_time = time.time()
# Pass the request to the next middleware or view
response = self.get_response(request)
# Calculate how long the request took
duration = time.time() - start_time
logger.info(
"%s %s completed in %.4f seconds (status %s)",
request.method,
request.get_full_path(),
duration,
response.status_code
)
return response
Using Python's logging module instead of print() gives you control over log levels, output destinations, and formatting. In production, logs can be directed to files, monitoring systems, or external services.
Step 2: Register the Middleware in settings.py
Add your middleware to the MIDDLEWARE list in middleware_project/settings.py. The position in the list determines when it runs relative to other middleware:
# middleware_project/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',
# Custom middleware: placed last to measure total processing time
'myapp.middleware.request_time_logging.RequestTimeLoggingMiddleware',
]
Middleware order matters. Middleware is executed top-to-bottom for requests and bottom-to-top for responses. If you place the timing middleware at the top of the list, it measures the time spent in all other middleware plus the view. If you place it at the bottom, it primarily measures view processing time. Choose the position based on what you want to measure.
Step 3: Create a Simple View to Test
Create a basic view in myapp/views.py:
from django.http import HttpResponse
def home(request):
return HttpResponse("Hello, this is the home page!")
Step 4: Configure URL Routing
Create myapp/urls.py:
from django.urls import path
from .views import home
urlpatterns = [
path('', home, name='home'),
]
Include the app URLs in middleware_project/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('myapp.urls')),
]
Step 5: Run the Server and Verify
python manage.py runserver
Visit http://127.0.0.1:8000/ in your browser. In the terminal, you should see log output similar to:
GET / completed in 0.0012 seconds (status 200)
Each time you refresh the page, a new log entry appears showing the request method, path, processing duration, and HTTP status code.
Adding Exception Handling to Your Middleware
A robust middleware should handle errors gracefully so that an exception inside the middleware itself doesn't crash the entire application:
import time
import logging
logger = logging.getLogger(__name__)
class RequestTimeLoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start_time = time.time()
response = self.get_response(request)
duration = time.time() - start_time
logger.info(
"%s %s completed in %.4f seconds (status %s)",
request.method,
request.get_full_path(),
duration,
response.status_code
)
return response
def process_exception(self, request, exception):
"""Called when a view raises an unhandled exception."""
logger.error(
"Unhandled exception on %s %s: %s",
request.method,
request.get_full_path(),
str(exception),
exc_info=True
)
# Returning None lets Django's default exception handling take over
return None
The process_exception method is called only when a view raises an exception. Returning None allows Django to continue with its default error handling (e.g., showing a 500 error page).
Common Mistake: Forgetting to Call get_response
A critical error when writing middleware is forgetting to call self.get_response(request). This breaks the entire middleware chain and prevents the view from ever executing:
class BrokenMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# WRONG: never calls get_response: the view is never reached
from django.http import HttpResponse
return HttpResponse("This intercepts everything!")
Every request to any URL returns "This intercepts everything!" and no view logic ever runs.
The correct approach is to always call self.get_response(request) unless you intentionally want to short-circuit the request (e.g., for an authentication check that rejects unauthorized users):
class CorrectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Pre-processing logic here
# CORRECT: always pass the request down the chain
response = self.get_response(request)
# Post-processing logic here
return response
If your middleware does not call self.get_response(request), no subsequent middleware or view will execute. Only skip this call if you are deliberately blocking the request and returning your own response (e.g., returning a 403 for unauthorized access).
Best Practices for Django Middleware
Keep Middleware Lightweight
Middleware runs on every single request. Avoid database queries, external API calls, or heavy computations inside middleware unless absolutely necessary. Offload intensive tasks to views, background workers, or caching layers.
Handle Exceptions Gracefully
Always wrap risky operations in try/except blocks within your middleware. An unhandled exception in middleware can result in 500 errors for all requests, not just the ones related to your middleware's purpose.
Be Mindful of Ordering
Middleware executes in the order listed in MIDDLEWARE for requests and in reverse order for responses. Place authentication middleware before view-dependent middleware, and place logging/timing middleware where it captures the scope you need.
Make Middleware Reusable
Design your middleware to be configurable and self-contained so it can be reused across projects:
from django.conf import settings
class RequestTimeLoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# Allow the threshold to be configured in settings
self.slow_request_threshold = getattr(
settings, 'SLOW_REQUEST_THRESHOLD', 1.0
)
def __call__(self, request):
start_time = time.time()
response = self.get_response(request)
duration = time.time() - start_time
if duration > self.slow_request_threshold:
logger.warning("Slow request: %s %s took %.4f seconds",
request.method, request.get_full_path(), duration)
return response
Avoid Modifying the Request Object Unnecessarily
While you can add attributes to the request object in middleware, doing so excessively makes your views dependent on specific middleware being active. Keep modifications minimal and well-documented.
Summary
| Concept | Details |
|---|---|
| What middleware does | Intercepts every request/response flowing through Django |
| Minimum required methods | __init__(self, get_response) and __call__(self, request) |
| Registration | Add the dotted path to the MIDDLEWARE list in settings.py |
| Execution order | Top-to-bottom for requests, bottom-to-top for responses |
| Key rule | Always call self.get_response(request) unless intentionally blocking |
Custom middleware gives you a centralized, reusable way to handle cross-cutting concerns like logging, security, and performance monitoring across your entire Django application. By following the structure and best practices outlined in this guide, you can build middleware that is clean, efficient, and production-ready.