In Django, what do a middleware's sync_capable and async_capable attributes declare, and what are their defaults?
answer
- two booleans on the factory
- safe default is sync-only
- three decorators in django.utils.decorators
- both False fails at startup
basics
~20 sThey declare which request modes a Django middleware factory can handle: sync_capable defaults to True and async_capable to False, so an undecorated middleware is sync-only and Django adapts it with a thread hop under ASGI.
solid answer
~40 sThe flags are attributes on the middleware **factory** — class attributes on a class, function attributes on a function — and Django reads them with defaults `sync_capable=True`, `async_capable=False` when it builds the chain. A plain custom middleware is therefore sync-only; under ASGI Django wraps it with `async_to_sync`/`sync_to_async` at a thread-hop cost. `django.utils.decorators` provides `sync_only_middleware`, `async_only_middleware` and `sync_and_async_middleware` to set them on function factories. Both `False` makes `load_middleware()` raise `RuntimeError`. The flags are a promise, not a converter: if you declare `async_capable` your callable must actually be a coroutine function when `get_response` is one. Bundled middleware built on `MiddlewareMixin` sets both to `True`.
code
python · 30 linesfrom django.utils.decorators import (
async_only_middleware,
sync_and_async_middleware,
sync_only_middleware,
)
@sync_only_middleware # sync_capable=True, async_capable=False (the default)
def legacy_timing(get_response):
def middleware(request):
return get_response(request)
return middleware
@async_only_middleware # sync_capable=False, async_capable=True
def stream_guard(get_response):
async def middleware(request):
return await get_response(request)
return middleware
class RequestIdMiddleware:
sync_capable = True
async_capable = False # explicit; same as leaving both out
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)go deeper
Remember the two names and their defaults: sync_capable True, async_capable False. A custom middleware you write without flags is sync-only.
Explain that the flags sit on the factory, that Django reads them when it builds the chain, and that the three decorators in django.utils.decorators set them for function factories.
Show that the flags are a contract: declaring async support stops Django adapting you, so the code must really branch on get_response's kind. Audit third-party middleware flags before an ASGI move.
Treat middleware async capability as an adoption gate: an ASGI migration is only as concurrent as its least capable middleware, so set a policy that new middleware ships hybrid.
## What the two flags are A Django **middleware factory** is the thing you list in the `MIDDLEWARE` setting: either a class or a function that receives `get_response` once and returns the callable Django invokes per request. Since Django gained ASGI support, a factory can also declare **which kind of request it can handle** through two plain boolean attributes: - **`sync_capable`** — the middleware can run in **synchronous** mode, where `get_response` is an ordinary function and the middleware callable is an ordinary `def`. - **`async_capable`** — the middleware can run in **asynchronous** mode, where `get_response` is a coroutine function and the middleware callable must be `async def` (or be marked as a coroutine function). On a class they are class attributes; on a function factory they are attributes set on the function object. Django reads them with `getattr()` while it builds the middleware chain, so a factory that sets nothing gets the defaults. ## The defaults and what they mean | Attribute | Default when absent | Consequence | |---|---|---| | `sync_capable` | `True` | Every middleware is assumed to work in sync mode | | `async_capable` | `False` | No middleware is assumed to work in async mode | So an undecorated custom middleware is **sync-only**. That is the safe assumption: an old-style `def middleware(request): return get_response(request)` would break if Django handed it a coroutine function, because it would return an un-awaited coroutine instead of a response. Django's own bundled middleware is different. Most of it subclasses `django.utils.deprecation.MiddlewareMixin`, which sets **both** flags to `True`, and the Django docs state that the bundled middleware supports both sync and async requests. ## Setting the flags You can set the attributes by hand, but `django.utils.decorators` ships three decorators for **function** factories: 1. `sync_only_middleware` — sets `sync_capable = True`, `async_capable = False`. This is the default made explicit. 2. `async_only_middleware` — sets `sync_capable = False`, `async_capable = True`. 3. `sync_and_async_middleware` — sets both to `True`, for a **hybrid** middleware. On a class you simply write `sync_capable = True` and `async_capable = True` in the class body. If a factory ends up with both flags `False`, Django refuses to build the stack: `load_middleware()` raises a `RuntimeError` saying the middleware must have at least one of `sync_capable`/`async_capable` set to `True`. ## The flags are a promise, not a converter This is the point interviewers probe. Setting `async_capable = True` does not make your code asynchronous. It tells Django "if you hand me a coroutine `get_response`, I will return a coroutine function". Django then **stops adapting** that middleware in async mode. If the code behind the flag is still a plain blocking `def`, the next layer receives a coroutine where it expected a response, or a blocking call runs on the event loop. - A **sync-only** middleware under ASGI is wrapped by Django: its downstream `get_response` is converted with `async_to_sync`, and the stack switches into a thread for it. - An **async-only** middleware under WSGI is wrapped the other way: Django adapts around it with `sync_to_async` and `async_to_sync` so it can still run. - A **hybrid** middleware must inspect `get_response` (with `inspect.iscoroutinefunction`) and return the matching kind of callable. The `sync_and_async_middleware` decorator only sets the flags; it does not write the branch for you. ## Why this matters in practice Under WSGI (`WSGIHandler`) Django builds the chain in sync mode, and as long as no middleware is async-only, sync-capable middleware is never adapted. Under ASGI (`ASGIHandler`) Django builds it in async mode, and each sync-only middleware forces a switch between the event loop and a thread. The flags are therefore how a team finds out, before load testing, which of its middleware will cost a thread per request once the project runs on an ASGI server. With `DEBUG = True`, debug logging on the `django.request` logger prints a line such as "Asynchronous handler adapted for middleware …" for each middleware Django had to adapt. ## Summary - Two flags, both read from the factory: `sync_capable` (default `True`) and `async_capable` (default `False`). - Three decorators in `django.utils.decorators` set them on function factories. - Both `False` is a startup `RuntimeError`. - The flags declare capability; the code must actually match the mode Django chooses. - Bundled middleware based on `MiddlewareMixin` already declares both, so audits focus on custom and third-party middleware.
- Does decorating a Django middleware with sync_and_async_middleware make it work in async mode?No. The decorator only sets both flags to `True`. Django then stops adapting the middleware, so the factory must check `inspect.iscoroutinefunction(get_response)` and return an `async def` callable in async mode and a plain function otherwise. Without that branch, one of the two modes breaks.
- What happens if a Django middleware sets both sync_capable and async_capable to False?Django cannot run it in either mode, so `load_middleware()` raises a `RuntimeError` when the handler is built, saying the middleware must have at least one of `sync_capable`/`async_capable` set to `True`. It fails at startup, not on the first request.
saying these in an interview costs you the question
- Django middleware is async-capable by default
- Setting async_capable = True converts a sync middleware into async code
- The flags go on the middleware instance created per request
- Sync-only middleware simply cannot be used under ASGI
- Django's bundled middleware is sync-only and always adapted