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 logging
import time
logger = 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:

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