Skip to main content

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
MethodWhen It RunsPurpose
__init__(self, get_response)Once at server startupStore get_response and perform one-time setup
__call__(self, request)On every requestProcess the request before and/or after the view
process_exception(self, request, exception)When a view raises an exceptionHandle or log uncaught exceptions
process_template_response(self, request, response)When the view returns a TemplateResponseModify 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
tip

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

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
danger

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

ConceptDetails
What middleware doesIntercepts every request/response flowing through Django
Minimum required methods__init__(self, get_response) and __call__(self, request)
RegistrationAdd the dotted path to the MIDDLEWARE list in settings.py
Execution orderTop-to-bottom for requests, bottom-to-top for responses
Key ruleAlways 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.