skip to content

Sync & Async Adapters

Each middleware declares sync_capable and async_capable, and Django wraps mismatches in sync_to_async or async_to_sync at a thread-hop cost. Interviewers ask how to write one that serves both.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

4

How do you write a Django middleware, such as a tenant resolver, that runs in both WSGI and ASGI deployments without an adapter?

level: middleimportance: must knowfreq 45%

answer

  1. declare both, then branch
  2. decide once, at construction
  3. inspect.iscoroutinefunction(get_response)
  4. mark the instance as a coroutine

basics

~10 s

Declare a Django middleware hybrid with sync_and_async_middleware (or both class flags True), check inspect.iscoroutinefunction(get_response) once in the factory, and return an async def callable for async stacks and a plain function for sync ones.

solid answer

~40 s

Set `sync_capable = True` and `async_capable = True`, with `@sync_and_async_middleware` on a function factory or class attributes on a class. Then decide the mode **once**, at startup: if `inspect.iscoroutinefunction(get_response)` is true, return an `async def middleware(request)` that awaits `get_response` and uses only async-safe calls such as `await Tenant.objects.filter(...).afirst()`; otherwise return a plain function using the sync ORM. For a class, store `async_mode` in `__init__`, call `inspect.markcoroutinefunction(self)` when async, and have `__call__` return `self.__acall__(request)` in that mode — without the marker, layers above see the instance as sync. Django then never wraps the middleware, though it may still call it in sync mode if a sync-only middleware sits between it and the view.

code

python · 35 lines
python
from inspect import iscoroutinefunction

from django.http import HttpResponseNotFound
from django.utils.decorators import sync_and_async_middleware

from tenants.models import Tenant


def subdomain_of(request):
    return request.get_host().split(":")[0].split(".")[0]


@sync_and_async_middleware
def tenant_middleware(get_response):
    if iscoroutinefunction(get_response):

        async def middleware(request):
            qs = Tenant.objects.filter(subdomain=subdomain_of(request))
            tenant = await qs.afirst()
            if tenant is None:
                return HttpResponseNotFound("Unknown tenant")
            request.tenant = tenant
            return await get_response(request)

    else:

        def middleware(request):
            qs = Tenant.objects.filter(subdomain=subdomain_of(request))
            tenant = qs.first()
            if tenant is None:
                return HttpResponseNotFound("Unknown tenant")
            request.tenant = tenant
            return get_response(request)

    return middleware

go deeper

for a junior

Know that one middleware can serve both modes if it declares both flags and returns the right kind of function for the get_response it receives.

for a middle

Write the factory branch on inspect.iscoroutinefunction(get_response), keep each branch to its own kind of I/O, and explain why the decision happens once at construction.

for a senior

Explain the coroutine marker for class instances and why hybrid middleware can still run sync next to a sync-only neighbour; verify with django.request debug logs.

for a principal

Weigh maintaining two code paths per middleware against the thread cost of adapters; for shared middleware libraries, hybrid should be the default contract.

## The requirement A **hybrid** Django middleware runs without any adapter whether the project is served by a WSGI server (sync requests) or an ASGI server (async requests). Two things make it hybrid: 1. **Declare it**: the factory must have `sync_capable = True` and `async_capable = True`. On a function factory, `django.utils.decorators.sync_and_async_middleware` sets both. 2. **Match the mode**: Django passes the factory a `get_response`. If that is a coroutine function, the returned callable must be a coroutine function too; otherwise it must be a plain function. The factory decides **once**, at startup, with `inspect.iscoroutinefunction(get_response)`. The declaration alone is not enough. The `sync_and_async_middleware` decorator only sets two attributes; Django then trusts the middleware and does not wrap it, so a single-path body would be wrong in one of the two modes. ## A function-based tenant resolver Tenant resolution is a good fit: every request needs `request.tenant`, and resolving it is I/O (a database or cache lookup) that should not block the event loop in async mode. ```python from inspect import iscoroutinefunction from django.http import HttpResponseNotFound from django.utils.decorators import sync_and_async_middleware from tenants.models import Tenant def subdomain_of(request): return request.get_host().split(":")[0].split(".")[0] @sync_and_async_middleware def tenant_middleware(get_response): if iscoroutinefunction(get_response): async def middleware(request): qs = Tenant.objects.filter(subdomain=subdomain_of(request)) tenant = await qs.afirst() if tenant is None: return HttpResponseNotFound("Unknown tenant") request.tenant = tenant return await get_response(request) else: def middleware(request): qs = Tenant.objects.filter(subdomain=subdomain_of(request)) tenant = qs.first() if tenant is None: return HttpResponseNotFound("Unknown tenant") request.tenant = tenant return get_response(request) return middleware ``` Points to notice: - The branch runs **once**, when Django builds the stack, not per request. - Each branch uses only calls of its own kind: `await qs.afirst()` in the async one, `qs.first()` in the sync one. Calling the sync ORM from the async branch raises `SynchronousOnlyOperation`. - Short-circuiting (returning a 404 without calling `get_response`) works the same way in both branches. ## The class-based form and the coroutine marker A class is often more convenient when the middleware has helpers or configuration. The catch is that an **instance** with an `async def __call__` is not recognised as a coroutine function by `inspect.iscoroutinefunction()`. Django wraps every middleware instance in an exception-to-response converter that inspects its target this way, and a hybrid middleware above yours inspects its own `get_response` the same way. An unmarked async instance therefore looks synchronous: the layer above takes its sync path and receives an un-awaited coroutine instead of a response. The fix is `inspect.markcoroutinefunction(self)`, the same pattern Django's `MiddlewareMixin` uses: ```python from inspect import iscoroutinefunction, markcoroutinefunction class TenantMiddleware: sync_capable = True async_capable = True def __init__(self, get_response): self.get_response = get_response self.async_mode = iscoroutinefunction(get_response) if self.async_mode: markcoroutinefunction(self) def __call__(self, request): if self.async_mode: return self.__acall__(request) request.tenant = resolve_tenant(request) return self.get_response(request) async def __acall__(self, request): request.tenant = await aresolve_tenant(request) return await self.get_response(request) ``` `__call__` stays a plain `def` (so the sync path works) and returns the coroutine from `__acall__` in async mode, which the caller awaits. ## What you get, and what you do not | Deployment | What Django does with the hybrid middleware | |---|---| | WSGI | Calls the sync branch directly; no adapter | | ASGI, all other middleware async-capable | Calls the async branch directly; no thread is held while the view runs | | ASGI, a sync-only middleware sits between you and the view | Django may call you in **sync** mode, because it minimises switches | That last row surprises people: the docs note that a hybrid middleware may be called in a mode that does not match the view, because Django optimises the whole chain for the fewest sync/async transitions. Also be honest about the lookup itself. Removing the middleware adapter does not make every call inside non-blocking; Django's async ORM methods such as `afirst()` still run the query through its sync-to-async bridge. What the hybrid design removes is the **adapter around the middleware**, which otherwise keeps a thread for the whole downstream call. ## Checklist - Both flags `True`, via the decorator or class attributes. - One `iscoroutinefunction(get_response)` decision at construction time. - Async branch: `async def`, `await get_response(request)`, async-safe I/O only. - Class form: `markcoroutinefunction(self)` in async mode. - Verify with `DEBUG = True` and debug logging for `django.request`: no "handler adapted" line should name your middleware.

  • Why must a class-based async Django middleware call inspect.markcoroutinefunction(self)?
    `inspect.iscoroutinefunction()` returns False for an instance even when its class defines `async def __call__`. Django's exception-to-response wrapper, and any hybrid middleware above, inspect `get_response` that way, so an unmarked instance looks sync: they take the sync path and get an un-awaited coroutine instead of a response. Marking the instance fixes the detection.
  • Can a hybrid Django middleware be called in sync mode even though the view is async?
    Yes. Django optimises the chain for the fewest sync/async switches, so if a sync-only middleware sits between the hybrid one and the view, the handler it receives is sync and it runs its sync branch. The docs warn that the call kind may not match the underlying view.
  • Does the async branch of a Django tenant middleware using afirst() avoid threads entirely?
    No. The middleware itself is no longer wrapped in an adapter, so no thread is held while the view runs, but Django's async ORM methods such as `afirst()` still execute the query through a sync-to-async bridge. The win is removing the per-request thread around the whole downstream call.

saying these in an interview costs you the question

  • Checking iscoroutinefunction(get_response) on every request instead of once at startup
  • Calling the synchronous ORM from the async branch
  • The decorator alone makes a sync function async-safe
  • An instance with async def __call__ is automatically detected as a coroutine function
  • A hybrid middleware is always called in the same mode as the view
open as a page

In Django, what do a middleware's sync_capable and async_capable attributes declare, and what are their defaults?

level: juniorimportance: should knowfreq 38%

basics

~20 s

They 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.

open as a page

After moving a Django project to ASGI with async views, every request still holds a thread and the log says 'Asynchronous handler adapted for middleware'; what is happening and how do you fix it?

level: seniorimportance: should knowfreq 33%

basics

~20 s

A sync-only middleware sits in the Django ASGI stack, so Django runs it and every sync-capable middleware outside it in a thread and reaches the async view through async_to_sync, holding that thread per request. Make it hybrid.

open as a page

How does Django's MiddlewareMixin run under ASGI, and why do its process_request and process_response hooks still hop into a thread?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Django's MiddlewareMixin is sync- and async-capable; under ASGI call returns acall, which awaits get_response directly but runs each sync process_request and process_response hook through sync_to_async, so it hops per hook instead of holding a thread.

open as a page