Peeling back the layers of Django to understand what actually happens when a request hits your server WSGI, middleware stacks, URL resolution, view dispatch, and the ORM query lifecycle.
Most Django tutorials teach you what to write. This one teaches you what Django actually does with it. Understanding the internals makes you a dramatically better debugger, architect, and performance engineer.
The Request Lifecycle
When a request arrives, it travels through: the WSGI/ASGI server → Django's handler → the middleware stack → URL resolution → view invocation → the middleware stack (response phase) → the WSGI server again. Every layer can short-circuit this chain.
WSGI Under the Hood
Django's WSGI app is a simple callable. When Gunicorn or uWSGI receives an HTTP request, it calls django.core.handlers.wsgi.WSGIHandler with an environ dict and a start_response callable. Django builds an HttpRequest from the environ, runs the middleware chain, and writes the response back.
# Simplified WSGIHandler.__call__
def __call__(self, environ, start_response):
request = self.request_class(environ)
response = self.get_response(request) # middleware chain
status = '%d %s' % (response.status_code, response.reason_phrase)
response_headers = list(response.items())
start_response(status, response_headers)
return responseHow Middleware Actually Works
Middleware is implemented as a chain of callables. Django wraps each middleware class around the next one, innermost-first. This is why middleware order in settings.py matters each class's __call__ wraps the next handler.
# Building the middleware chain (simplified from django/core/handlers/base.py)
def _get_response(self, request):
handler = convert_exception_to_response(self._inner_middleware_chain)
return handler(request)
# Django stacks them like:
# middleware_N( middleware_N-1( ... middleware_1( view ) ) )Writing Your Own Middleware
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
import time
start = time.perf_counter()
response = self.get_response(request) # call the next layer
duration = time.perf_counter() - start
response['X-Response-Time'] = f'{duration * 1000:.2f}ms'
return responseURL Resolution Deep Dive
Django's URL resolver walks through urlpatterns in order, trying to match the path using compiled regular expressions (or route converters). Once matched, it extracts kwargs and calls the associated view. If no pattern matches, it raises a Resolver404, which the middleware catches.
# How Django's resolver core works (simplified)
def resolve(self, path):
for pattern in self.url_patterns:
try:
match = pattern.resolve(path)
if match:
return match
except Resolver404 as e:
pass
raise Resolver404({"tried": tried, "path": path})The ORM Query Lifecycle
When you write User.objects.filter(is_active=True), Django constructs a Q object internally. The QuerySet is lazy no SQL is issued until you evaluate it (iterate, slice, call .exists(), etc.). Django's SQL compiler then translates the Q tree into SQL based on your database backend.
qs = User.objects.filter(is_active=True)
# No SQL yet qs is a QuerySet object
print(qs.query)
# SELECT "auth_user"."id", ... FROM "auth_user"
# WHERE "auth_user"."is_active" = True
# Evaluation:
users = list(qs) # SQL fires here
first = qs[0] # SQL fires here (LIMIT 1)
count = qs.count()# SQL fires here (COUNT(*))Signal Dispatch Internals
Django signals use a weak-reference registry (`_live_receivers`) to avoid memory leaks. When post_save fires, Django iterates the receiver list, checks dispatch_uid for deduplication, and calls each function synchronously. This is why slow signal handlers block your request cycle.
- Use dispatch_uid to prevent duplicate receivers when modules are imported multiple times.
- Move any I/O-heavy signal work into Celery tasks signals are synchronous.
- Use post_save over pre_save when you need the instance's primary key to be assigned.
Template Engine Pipeline
Django's template engine compiles a template string into a NodeList on first use. Each Node's render() method is called with the context, producing a string. The compiled template is cached in memory so subsequent renders skip the compilation step this is why template rendering is fast on warm instances.
Knowing what happens below the surface transforms guesswork into informed decisions. When performance degrades or behaviour is unexpected, you'll know exactly which layer to look at and that saves hours.