Writing Custom Middleware
Hook into every request and response for cross-cutting concerns like timing, request IDs and tenant resolution.
Middleware is a layer that wraps every request on the way in and every response on the way out. It is the right home for concerns that apply to the whole site: request logging, timing headers, correlation IDs, tenant resolution, forcing a locale. If you find yourself copying the same two lines into every view, it probably belongs in middleware.
The modern middleware shape
Since Django 1.10 middleware is a callable factory. You get the request before the view runs and the response after — an onion, each layer wrapping the next.
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response # called ONCE at startup
def __call__(self, request):
import time
start = time.monotonic()
response = self.get_response(request) # run the view (and inner middleware)
response['X-Response-Time-ms'] = int((time.monotonic() - start) * 1000)
return responseOrder matters: middleware listed first in MIDDLEWARE is outermost. Security middleware goes near the top, your app-specific ones lower. You can also implement process_exception() and process_view() hooks for finer control.
Example
import time
import uuid
import logging
from django.utils.deprecation import MiddlewareMixin
logger = logging.getLogger('request')
class RequestContextMiddleware:
"""Attach a correlation ID, resolve the tenant, and log timing — once per request."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# --- inbound: enrich the request ---
request.id = request.headers.get('X-Request-ID', uuid.uuid4().hex)
host = request.get_host().split(':')[0]
request.tenant = Tenant.objects.filter(domain=host).first()
start = time.monotonic()
response = self.get_response(request)
# --- outbound: annotate and log ---
elapsed_ms = int((time.monotonic() - start) * 1000)
response['X-Request-ID'] = request.id
response['X-Response-Time-ms'] = str(elapsed_ms)
logger.info(
'request',
extra={
'request_id': request.id,
'path': request.path,
'method': request.method,
'status': response.status_code,
'ms': elapsed_ms,
'tenant': getattr(request.tenant, 'slug', None),
},
)
return response
def process_exception(self, request, exception):
# Correlate the crash with the same request id and re-raise.
logger.exception('unhandled', extra={'request_id': getattr(request, 'id', '-')})
return None # let Django's normal 500 handling proceed
# settings.py — order is outermost-first:
# MIDDLEWARE = [
# 'django.middleware.security.SecurityMiddleware',
# 'core.middleware.RequestContextMiddleware',
# ...
# ]When to use it
- A developer writes middleware to attach the current user's active tenant to every request for a multi-tenant SaaS app.
- A team adds request-timing middleware that logs response time and request path to a monitoring service.
- A developer uses middleware to enforce a maintenance mode by returning a 503 page without modifying any view.
More examples
Simple middleware class
Measures the time between receiving the request and returning the response and adds it as a header.
class RequestTimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
import time
start = time.monotonic()
response = self.get_response(request)
elapsed = (time.monotonic() - start) * 1000
response["X-Response-Time"] = f"{elapsed:.1f}ms"
return responseRegister middleware in settings
Inserts the custom middleware class into the MIDDLEWARE list; order matters for request/response flow.
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"myapp.middleware.RequestTimingMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
# ...
]Maintenance mode middleware
Returns a 503 response for every request when MAINTENANCE_MODE is True in settings.
from django.http import HttpResponse
class MaintenanceModeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
from django.conf import settings
if getattr(settings, "MAINTENANCE_MODE", False):
return HttpResponse("Down for maintenance", status=503)
return self.get_response(request)
Discussion