Skip to content

Python Django: How to Set Up Custom Middleware in Django

How to create, register, and test custom Django middleware for request/response processing, logging, performance monitoring, and security.

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

    def __call__(self, request):

        response = self.get_response(request)


        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:


INSTALLED_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):
        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

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 = [
    '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',
    'myapp.middleware.request_time_logging.RequestTimeLoggingMiddleware',
]

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
        )
        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):
        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):

        response = self.get_response(request)


        return response

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

Navigation