Skip to content
C Codeloom

Courses / Django Full Stack Web Development

Lesson 10 of 13

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.

Intermediate 9 min read

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
Django request lifecycle

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. AuthenticationMiddleware must run after SessionMiddleware, otherwise request.user will 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_view when 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 RequestFactory or 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 = True and 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.

Progress is saved locally to your browser.