Django middleware is a lightweight plugin layer that sits between the web server and your views, processing every HTTP request on the way in and every response on the way out. Each middleware is a callable that receives a get_response function and returns a function that takes a request and returns a response — letting you modify requests, responses, authentication, security headers, sessions, and errors globally without editing individual views.
This guide is rewritten for Django 5.x (Django 5.2 is the current LTS in 2026). It covers the modern MIDDLEWARE setting, the new-style factory pattern, ordering rules, the optional hook methods, async-capable middleware, and how to migrate legacy MIDDLEWARE_CLASSES code.
Key takeaways
- Middleware processes requests top-to-bottom and responses bottom-to-top through the
MIDDLEWARElist. - Modern middleware is a factory:
def simple_middleware(get_response)returning an innermiddleware(request)callable — no base class required. - The pre-1.10
MIDDLEWARE_CLASSESsetting (withprocess_request/process_response) was removed in Django 2.0; current projects useMIDDLEWARE. - Optional hooks —
process_view,process_exception, andprocess_template_response— give you finer control alongside__call__. - Middleware can be async-capable using the
sync_capable/async_capableflags plusmarkcoroutinefunction/iscoroutinefunction. - Order matters:
SecurityMiddlewarebelongs first, andSessionMiddlewaremust come beforeAuthenticationMiddleware.
What is middleware in Django?
Middleware is a framework of hooks into Django's request/response processing. It is a chain of callables wrapped around your view: a request passes down through every enabled middleware before it reaches the view, and the view's response passes back up through the same chain in reverse before it is returned to the browser.
Because middleware runs for every request, it is the right home for cross-cutting concerns that shouldn't be duplicated in each view — security headers, session loading, authentication, CSRF protection, locale selection, GZip compression, logging, and custom request enrichment. For behaviour that only applies to certain views, you usually want a custom decorator to check user roles and permissions instead, which keeps the logic local to the views that need it.
How does the middleware request/response cycle work?
Think of the MIDDLEWARE list as an onion wrapped around the view. On the way in, Django calls each middleware from the top of the list downward; on the way out, the response travels back up from the bottom to the top. The same middleware therefore sees the request before the view runs and the response after it.
In a factory-style middleware, the code you write before calling get_response(request) is the request phase, and the code after it is the response phase:
import time
def simple_middleware(get_response):
# One-time configuration and initialisation (runs once at startup).
def middleware(request):
# --- Request phase: runs on the way IN, before the view. ---
start = time.monotonic()
response = get_response(request)
# --- Response phase: runs on the way OUT, after the view. ---
response["X-Response-Time"] = f"{time.monotonic() - start:.3f}s"
return response
return middlewareHere get_response is either the next middleware in the chain or, for the innermost middleware, the view itself. Calling it advances the request one step deeper into the onion; its return value is the response bubbling back out. A middleware can also short-circuit the chain by returning a response without calling get_response — that's how CsrfViewMiddleware can reject a forged request before it ever reaches your view.
How do you write custom middleware?
You can write middleware as a factory function (above) or as a class. The class form is handy when you want to keep state on the instance or implement the optional hook methods. A class needs __init__(self, get_response) and __call__(self, request), and may add hooks such as process_exception:
import logging
import time
logger = logging.getLogger("requests")
class TimingMiddleware:
def __init__(self, get_response):
# Runs once when the web server starts.
self.get_response = get_response
def __call__(self, request):
# Request phase (on the way in, top-to-bottom).
request.start_time = time.monotonic()
response = self.get_response(request)
# Response phase (on the way out, bottom-to-top).
duration = time.monotonic() - request.start_time
response["X-Response-Time"] = f"{duration:.3f}s"
return response
def process_exception(self, request, exception):
# Called only if a view raises an uncaught exception.
logger.error("View error on %s: %s", request.path, exception)
return None # None lets Django's normal exception handling continueMiddleware hook methods
Beyond __call__, a middleware class can implement optional hooks that Django calls at specific points. These provide the granular control the old MIDDLEWARE_CLASSES API offered, but on top of the new-style callable:
| Hook | When it runs | Return value |
|---|---|---|
__call__(self, request) |
Wraps the whole request (required) | The HttpResponse |
process_view(request, view_func, view_args, view_kwargs) |
Just before Django calls the view | None to continue, or an HttpResponse to short-circuit |
process_exception(request, exception) |
When a view raises an uncaught exception | None to fall through, or an HttpResponse |
process_template_response(request, response) |
After the view, if the response has a render() method |
A response object (must return one) |
For full control over what users see when something goes wrong, pair process_exception with custom error pages in Django.
What's the order of MIDDLEWARE?
Order is significant because each middleware depends on what runs before it. For example, AuthenticationMiddleware needs the session loaded first, so SessionMiddleware must appear earlier in the list. To activate your own middleware, add its dotted import path to the MIDDLEWARE list at the position that matches what it does:
# 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",
# Your custom middleware, placed after auth so request.user is available:
"my_app.middleware.TimingMiddleware",
]Default Django MIDDLEWARE and what each does
A fresh Django 5.x project ships with this default stack:
| Middleware | Responsibility |
|---|---|
SecurityMiddleware |
SSL redirects, HSTS, and other security headers; goes first |
SessionMiddleware |
Loads and saves the session across requests |
CommonMiddleware |
URL rewriting (APPEND_SLASH), DISALLOWED_USER_AGENTS, ETags |
CsrfViewMiddleware |
Adds and validates CSRF tokens to block cross-site request forgery |
AuthenticationMiddleware |
Attaches request.user based on the session |
MessageMiddleware |
Enables the cookie/session-based messages framework |
XFrameOptionsMiddleware |
Sets X-Frame-Options to protect against clickjacking |
Because request processing runs top-to-bottom, SecurityMiddleware sees requests first; because response processing runs bottom-to-top, SecurityMiddleware also gets the last word on outgoing headers.
How do you migrate legacy MIDDLEWARE_CLASSES code?
Before Django 1.10 there were five separate hooks — process_request, process_view, process_exception, process_template_response, and process_response — and middleware was registered under MIDDLEWARE_CLASSES. The first two ran before the view and the last three after it. Django 2.0 removed MIDDLEWARE_CLASSES entirely, so every project must use the MIDDLEWARE setting and the factory style.
Here is the difference at a glance:
| Aspect | Old-style (pre-1.10, removed) | New-style (Django 1.10+, current) |
|---|---|---|
| Setting | MIDDLEWARE_CLASSES |
MIDDLEWARE |
| Shape | Class with process_request/process_response |
Callable factory taking get_response |
| Base class | Plain class | None required (or MiddlewareMixin) |
| Phasing | Separate request/response methods | Code before/after get_response() |
| Async support | No | Yes (async_capable) |
If you have old middleware you can't immediately rewrite, wrap it with MiddlewareMixin, which adapts the old process_request/process_response methods to the new callable interface:
from django.utils.deprecation import MiddlewareMixin
class CustomMiddleware(MiddlewareMixin):
def process_request(self, request):
# Runs before the view (legacy hook, adapted by MiddlewareMixin).
pass
def process_response(self, request, response):
# Runs after the view; must return the response.
return responseDoes Django middleware support async?
Yes. Since Django 3.1, middleware can run under ASGI without blocking the event loop, and that support is mature in Django 5.x. Django inspects two class attributes — sync_capable and async_capable — to decide how to call your middleware, and it adapts (wrapping in a thread or an async shim) when a middleware supports only one mode. MiddlewareMixin sets both to True.
For a hand-written class, declare the flags and use markcoroutinefunction/iscoroutinefunction from asgiref.sync so Django can detect and call the async path. (For function-style middleware, the @sync_and_async_middleware decorator from django.utils.decorators does the equivalent.)
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
class AsyncCapableMiddleware:
# Tell Django this middleware works in both modes.
sync_capable = True
async_capable = True
def __init__(self, get_response):
self.get_response = get_response
# If the chain below us is async, mark this instance as a coroutine.
if iscoroutinefunction(self.get_response):
markcoroutinefunction(self)
def __call__(self, request):
if iscoroutinefunction(self.get_response):
return self.__acall__(request)
# Synchronous path.
response = self.get_response(request)
return response
async def __acall__(self, request):
# Asynchronous path.
response = await self.get_response(request)
return responsePutting it together
Middleware is one of Django's most powerful extension points: a small, ordered chain that lets you apply security, logging, and request enrichment across an entire app from one place. Reach for middleware when behaviour must apply to every request; reach for narrower tools — Django signals, custom management commands, or permissions and groups — when the concern is local to a model, a command, or a view.
Building or modernising a Django application and want middleware, security, and architecture done right? Our team offers Django development services for startups and enterprises, shipping production features in days to weeks. We've delivered 50+ projects over 12+ years across the USA, UK, Australia, Canada, Singapore, and beyond.
Frequently Asked Questions
What is middleware in Django?
Django middleware is a plugin layer that processes every HTTP request before it reaches a view and every response before it returns to the client. Each middleware is a callable that takes a get_response function and returns a function accepting a request and returning a response, making it ideal for global concerns such as authentication, security headers, sessions, and logging.
How do I write custom middleware in Django 5.x?
Write a factory function def my_middleware(get_response): that defines an inner def middleware(request):, runs your request-phase code, calls response = get_response(request), runs your response-phase code, and returns the response. Alternatively use a class with __init__(self, get_response) and __call__(self, request). Then add the dotted import path to the MIDDLEWARE list in settings.py.
What is the difference between MIDDLEWARE and MIDDLEWARE_CLASSES?
MIDDLEWARE_CLASSES was the pre-1.10 setting where each middleware was a class implementing process_request, process_response, and related hooks. MIDDLEWARE, introduced in Django 1.10, uses the new-style factory pattern that takes get_response. Django 2.0 removed MIDDLEWARE_CLASSES completely, so all current projects, including Django 5.x, must use MIDDLEWARE.
In what order does Django run middleware?
Django processes the MIDDLEWARE list top-to-bottom for incoming requests and bottom-to-top for outgoing responses, so the first middleware listed sees the request first and the response last. Order matters: SessionMiddleware must precede AuthenticationMiddleware, and SecurityMiddleware is placed first so it handles requests before anything else.
What are process_view, process_exception, and process_template_response?
They are optional hook methods a middleware class can implement. process_view runs just before the view is called, process_exception runs when a view raises an uncaught exception, and process_template_response runs after the view if the response has a render() method. Each can return None to continue or an HttpResponse to short-circuit, except process_template_response, which must return a response object.
Does Django middleware support async views?
Yes. Since Django 3.1, middleware can be async-capable. Declare the sync_capable and async_capable class attributes and use markcoroutinefunction/iscoroutinefunction from asgiref.sync so Django calls the correct path. Django automatically adapts middleware that supports only one mode, but mixing forces conversions, so matching your middleware to your stack (ASGI vs WSGI) gives the best performance.