Django Middleware and the Request Lifecycle
Understand how a Django request flows through middleware, URL routing, and views, and learn to write custom middleware for cross-cutting concerns.
What you'll learn
- ✓The exact path of a Django request
- ✓How middleware wraps views
- ✓When to write custom middleware
- ✓The difference between process_request and process_response
- ✓How to short-circuit a response
Prerequisites
- •Comfortable with Django views and settings
What and Why
Middleware is the layer that wraps every view in your project. Authentication, sessions, CSRF, gzip, locale detection, and security headers all live there. Knowing the request lifecycle helps you debug mysterious behavior like “why is my header missing” or “why does my view see an anonymous user.”
Mental Model
A Django request flows down a stack of middleware on the way in and back up on the way out. Each middleware is a callable that takes get_response and returns a new callable. The view runs at the bottom of the stack.
Request
|
v
[SecurityMiddleware] -> may redirect to HTTPS
[SessionMiddleware] -> attaches request.session
[AuthenticationMiddleware] -> attaches request.user
[CsrfViewMiddleware] -> validates CSRF token
|
v
URL Resolver -> View -> Response
|
v
[CsrfViewMiddleware] -> sets cookie if needed
[AuthenticationMiddleware]
[SessionMiddleware] -> saves session
[SecurityMiddleware] -> adds headers
|
v
Response The order in MIDDLEWARE matters. The first middleware listed is the outermost wrapper. Its process_request part runs first and its response handling runs last.
Hands-on Example
Let’s write a middleware that logs request timing and adds an X-Response-Time header.
# myapp/middleware.py
import time
import logging
logger = logging.getLogger(__name__)
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response # one-time setup
def __call__(self, request):
start = time.perf_counter()
response = self.get_response(request)
elapsed_ms = (time.perf_counter() - start) * 1000
response["X-Response-Time"] = f"{elapsed_ms:.1f}ms"
logger.info("%s %s -> %d in %.1fms",
request.method, request.path,
response.status_code, elapsed_ms)
return response
Register it in settings.py. Place timing near the top so it captures the full pipeline.
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"myapp.middleware.TimingMiddleware",
"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",
]
Short-circuit example. Block an IP without ever reaching a view.
from django.http import HttpResponseForbidden
BLOCKED = {"203.0.113.7"}
class BlockIPMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
ip = request.META.get("REMOTE_ADDR")
if ip in BLOCKED:
return HttpResponseForbidden("Blocked")
return self.get_response(request)
You can also hook into view-level behavior with these optional methods.
class HookMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def process_view(self, request, view_func, view_args, view_kwargs):
# Inspect or replace the view. Return None to continue.
return None
def process_exception(self, request, exception):
# Convert an exception into a response, or return None.
return None
Common Pitfalls
- Misordering middleware.
AuthenticationMiddlewaremust run afterSessionMiddleware, otherwiserequest.userwill not be set. - Forgetting that streaming responses do not let you read or modify the body in a downstream middleware. Set headers instead.
- Doing slow work in middleware. Every request pays the cost. Move heavy logic to async tasks or selective decorators.
- Raising exceptions instead of returning responses. Middleware errors propagate to all requests.
Practical Tips
- Use
process_viewwhen you want access to the resolved view but not to actually run it. - Build middleware as small focused units. Combine via configuration, not inheritance.
- Test middleware with Django’s
RequestFactoryor the test client to verify both branches: pass-through and short-circuit. - Keep mutating side effects on the response, not the request, when possible. It makes the data flow easier to follow.
- For async views, write async middleware by adding
async_capable = Trueand supporting both sync and async calls.
Wrap-up
Middleware is the right place for cross-cutting concerns, and understanding the lifecycle helps you debug authentication, CSRF, and header bugs in minutes instead of hours. Keep middleware lean, ordered intentionally, and well tested, and the rest of your views can stay focused on business logic.
Related articles
- Django Django Admin Customization: A Practical Tutorial
Go beyond the default Django admin: customize ModelAdmin classes, list views, search, filters, inline editing, and admin actions to build a usable backoffice your team will actually enjoy.
- Django Django Caching Strategies
Compare per-view, template fragment, low-level, and per-site caching in Django and learn when each pays off.
- Django Django Celery Task Queue Tutorial
A practical guide to wiring Celery into Django for background work, scheduled jobs, and reliable task processing.
- Django Django Class-Based Views: A Practical Tutorial
Understand Django's class-based views by building from View up to ListView and UpdateView. Learn the MRO, mixins, and when CBVs beat function-based views in real projects.