Hello,
I have a production Django DRF (django-rest-framework) server application that has one endpoint that would benefit greatly from being async due to making external HTTP requests in the View
. I decided on breaking this one particular endpoint out into a separate async only Django project, without DRF.
I got it working nicely using all async libraries like aiohttp
for external HTTP requests. I was very careful not to write any synchronous code so I was surprised to find 14 calls to the sync_to_async()
function for every request. It appears to be coming mostly from the default Django middleware. I thought the default middleware was updated to natively support async? Does this mean there are 14 context-switches, like a thread is pulled from the pool 14 times every request?
Even if I comment out all MIDDLEWARE
in settings.py
, it still makes 2 calls to sync_to_async()
to call the functions Signal.asend.<locals>.sync_send
and HttpResponseBase.close
.
Per Django docs:
However, if you put synchronous middleware between an ASGI server and an asynchronous view, it will have to switch into sync mode for the middleware and then back to async mode for the view. Django will also hold the sync thread open for middleware exception propagation. This may not be noticeable at first, but adding this penalty of one thread per request can remove any async performance advantage.
I thought getting rid of all middleware would avoid any chance of synchronous function calls.
My goal here is to avoid any synchronous code and avoid extra threads being used. Calling sync_to_async()
14 times per request seems like a lot of overhead. I was under the impression that if all of your Django View
code is asynchronous, then using extra threads are not needed. Even if I eliminate all middleware, it is still making synchronous calls, like to HttpResponseBase.close
.
Is it possible for Django v5 to be truly async at this point? Or do I have something setup wrong?
Below is the simple example code I used.
I added a print()
to venv312/Lib/site-packages/asgiref/sync.py
:
def sync_to_async(func, *, thread_sensitive = True, executor):
print("LOOK >>> sync_to_async() was called, the func is:")
print(func)
if func is None:
return lambda f: SyncToAsync(f, thread_sensitive=thread_sensitive, executor=executor)
return SyncToAsync(func, thread_sensitive=thread_sensitive, executor=executor)
My testing code:
# Simple async view:
class MyView(django.views.View):
async def get(self, request: ASGIRequest) -> JsonResponse:
print("ASYNC 'GET' HANDLER FUNCTION STARTS HERE...")
return JsonResponse({'message': 'hi'})
Uvicorn ASGI server:
./venv312/Scripts/python -X dev -m uvicorn my_django_project.asgi:application
Logs:
With default Django middleware 14 calls to sync_to_async()
are made:
INFO: Application startup complete.
# Make request:
# http -v GET http://127.0.0.1:8000/api/v1/test/
Executing <Task pending name='Task-1' coro=<Server.serve() running at C:\Users\server03\my_django_project\venv312\Lib\site-packages\uvicorn\server.py:69> cb=[_run_until_complete_cb() at C:\Program Files\Python312\Lib\asyncio\base_events.py:182] created at C:\Program Files\Python312\Lib\asyncio\runners.py:100> took 0.125 seconds
LOOK >>> sync_to_async() was called, the func is:
<function Signal.asend.<locals>.sync_send at 0x000001F74A4A1610>
LOOK >>> sync_to_async() was called, the func is:
<bound method SecurityMiddleware.process_request of <SecurityMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method SessionMiddleware.process_request of <SessionMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method CommonMiddleware.process_request of <CommonMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method CsrfViewMiddleware.process_request of <CsrfViewMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method AuthenticationMiddleware.process_request of <AuthenticationMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method MessageMiddleware.process_request of <MessageMiddleware
get_response=convert_exception_to_response.<locals>.inner>>
Executing <Task pending name='Task-7' coro=<ASGIHandler.handle.<locals>.process_request() running at C:\Users\server03\my_django_project\venv312\Lib\site-packages\django\core\handlers\asgi.py:193> wait_for=<Future pending cb=[shield.<locals>._outer_done_callback() at C:\Program Files\Python312\Lib\asyncio\tasks.py:922, Task.task_wakeup()] created at C:\Program Files\Python312\Lib\asyncio\base_events.py:449> cb=[_wait.<locals>._on_completion() at C:\Program Files\Python312\Lib\asyncio\tasks.py:534] created at C:\Program Files\Python312\Lib\asyncio\tasks.py:420> took 0.250 seconds
ASYNC 'GET' HANDLER FUNCTION STARTS HERE...
LOOK >>> sync_to_async() was called, the func is:
<bound method XFrameOptionsMiddleware.process_response of <XFrameOptionsMiddleware get_response=BaseHandler._get_response_async>>
LOOK >>> sync_to_async() was called, the func is:
<bound method MessageMiddleware.process_response of <MessageMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method CsrfViewMiddleware.process_response of <CsrfViewMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method CommonMiddleware.process_response of <CommonMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method SessionMiddleware.process_response of <SessionMiddleware get_response=convert_exception_to_response.<locals>.inner>>
LOOK >>> sync_to_async() was called, the func is:
<bound method SecurityMiddleware.process_response of <SecurityMiddleware get_response=convert_exception_to_response.<locals>.inner>>
INFO: 127.0.0.1:60566 - "GET /api/v1/test/ HTTP/1.1" 200 OK
LOOK >>> sync_to_async() was called, the func is:
<bound method HttpResponseBase.close of <JsonResponse status_code=200, "application/json">>
With all MIDDLEWARE
commented out in settings.py
, 2 sync_to_sync()
calls are made:
INFO: Application startup complete.
# Make request:
# http -v GET http://127.0.0.1:8000/api/v1/test/
Executing <Task pending name='Task-1' coro=<Server.serve() running at C:\Users\server03\my_django_project\venv312\Lib\site-packages\uvicorn\server.py:69> cb=[_run_until_complete_cb() at C:\Program Files\Python312\Lib\asyncio\base_events.py:182] created at C:\Program Files\Python312\Lib\asyncio\runners.py:100> took 0.140 seconds
LOOK >>> sync_to_async() was called, the func is:
<function Signal.asend.<locals>.sync_send at 0x0000018DED0CB4D0>
ASYNC 'GET' HANDLER FUNCTION STARTS HERE...
INFO: 127.0.0.1:64967 - "GET /api/v1/test/ HTTP/1.1" 200 OK
Executing <Task finished name='Task-7' coro=<ASGIHandler.handle.<locals>.process_request() done, defined at C:\Users\server03\my_django_project\venv312\Lib\site-packages\django\core\handlers\asgi.py:192> result=<JsonResponse...ication/json"> created at C:\Program Files\Python312\Lib\asyncio\tasks.py:420> took 0.265 seconds
LOOK >>> sync_to_async() was called, the func is:
<bound method HttpResponseBase.close of <JsonResponse status_code=200, "application/json">>
Thank you for your time!