Middleware in Django is a processing layer between the request and the view: every request passes through it before reaching the view, and every response passes through it before going back to the client. Writing a custom middleware takes just one class with __init__ and __call__, then a declaration in MIDDLEWARE in settings.py. This post builds a middleware that measures how long each request takes, logs it, and attaches an X-Process-Time header — ready to use in any Django 4.2+ project.
The problem
When an API is slow, the first question is always: where does the time go — the view, the queries, or somewhere else? Without measurements, you are only guessing: adding stray print calls inside views, or turning on a heavy profiler for the whole project. print in a view misses the time Django spends before and after the view (routing, built-in middleware, rendering the response), while a profiler is too noisy to run every day. You need a single measurement point that covers the whole request lifecycle, works on every endpoint once enabled, and requires no change to a single line of view code.
The solution
Django has used the new-style middleware since 1.10: a callable that takes get_response and returns a response. Create the file myapp/middleware.py:
import loggingimport timelogger = logging.getLogger(__name__)class RequestTimingMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): start = time.perf_counter() response = self.get_response(request) elapsed = time.perf_counter() - start response["X-Process-Time"] = f"{elapsed:.3f}" if elapsed > 0.5: logger.warning( "Slow request: %s %s took %.3fs", request.method, request.path, elapsed, ) else: logger.info("%s %s - %.3fs", request.method, request.path, elapsed) return response
Register it in settings.py, right after SecurityMiddleware:
MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "myapp.middleware.RequestTimingMiddleware", # ... các middleware còn lại]
Because the middleware wraps the whole request lifecycle, the measured time includes the view, the queries inside the view, and rendering the response. The X-Process-Time header lets you check instantly with curl or DevTools without opening the logs. The 0.5-second threshold only logs a warning for slow requests, so the logs do not flood under heavy traffic — tune this threshold to your project’s SLA.
Applying it to your project
Add this middleware to your project today, run it for a day, then filter the logs for endpoints that cross the 0.5-second threshold — that is your optimization priority list. For a slow endpoint, the next step is usually to inspect the queries: check whether the view has an N+1 query or is missing an index, instead of optimizing at random. Once you are comfortable, you can extend the same pattern into middleware that logs a request-id, blocks requests missing an auth header, or compresses responses — every middleware shares the same __init__/__call__ skeleton above.
Related posts
Three posts on the same Django performance topic on this blog:
- Django: sửa lỗi N+1 query bằng select_related trong 5 phút
- Django: bắt query chậm và dư thừa bằng django-debug-toolbar trong 5 phút
- Django: tăng tốc trang danh sách admin bằng list_select_related, xóa sổ hàng trăm query ẩn
FAQ
Does middleware run before or after the view?
Both. Code placed before the self.get_response(request) line runs before the view; code placed after it runs once the view has returned a response. That is how one middleware can both measure time and modify the response before it reaches the client.
Does the order in MIDDLEWARE matter?
Yes. Requests travel through the middleware top to bottom, and responses travel back bottom to top. The higher you place the timing middleware in the list (right after SecurityMiddleware), the more of the request lifecycle the number covers; place it lower and you miss the time spent in the middleware above it.
Can synchronous middleware work with async views?
Yes. Django automatically adapts synchronous middleware to run in an async context, so the class above works with both regular and async views. If your project is fully async and needs maximum performance, write async middleware with async def __call__ and await self.get_response(request).
Can middleware block a request?
Yes. Just return a response directly without calling get_response — for example, return a 403 when the request is missing an auth header. Note that the view and the middleware below will not run in that case, so only block when you really need to.
💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold good ideas into the post.
Sources
- Django documentation — Middleware: https://docs.djangoproject.com/en/6.1/topics/http/middleware/
