# sync\_to\_async() called 14 times with default Django middleware, and 2 times with no middleware

**URL:** <https://forum.djangoproject.com/t/sync-to-async-called-14-times-with-default-django-middleware-and-2-times-with-no-middleware/31701>\
**Category:** Async\
**Created:** [June 2, 2024, 9:18pm UTC](https://forum.djangoproject.com/t/sync-to-async-called-14-times-with-default-django-middleware-and-2-times-with-no-middleware/31701 "2024-06-02T21:18:42Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![rob4226](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/rob4226/32/21682_2.png) [@rob4226](https://forum.djangoproject.com/u/rob4226)\
**Post date:** [June 2, 2024, 9:18pm UTC](https://forum.djangoproject.com/t/sync-to-async-called-14-times-with-default-django-middleware-and-2-times-with-no-middleware/31701/1 "2024-06-02T21:18:42Z")

</div>

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](https://docs.djangoproject.com/en/5.0/topics/async/):

> 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`:

```python
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:

```python
# 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:

```sh
./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:

```python
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:

```python
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!

---

<div class="post-metadata">

**Author:** ![bigfootjon](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/bigfootjon/32/12068_2.png) [@bigfootjon](https://forum.djangoproject.com/u/bigfootjon)\
**Post date:** [June 6, 2024, 4:26am UTC](https://forum.djangoproject.com/t/sync-to-async-called-14-times-with-default-django-middleware-and-2-times-with-no-middleware/31701/2 "2024-06-06T04:26:16Z")

</div>

Hey there,

Thanks for doing this investigation! I think the summary of the answer is: not everything in Django has been asyncified yet. Some of the built-in components bridge the gap in support by reusing the sync version of the code to support async.

My understanding (and perhaps @andrewgodwin or someone can correct me if I’m wrong) is that the way these are implemented does _not_ require a separate thread per request (the scary bit from the docs you quoted above). Instead, these just use the threadpools from sync\_to\_async machinery which… you’re going to hit no matter what right now due to the database connection layers still being sync (there is an async interface for the ORM, but it’s skin-deep. All the internals are sync-only).

> 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?

Yeah partially correct. They’ve been modified such that they do NOT create a thread per request, but they still run synchronously internally.

There’s 3 different parts of the removal of `sync_to_async` calls in Django:

## 1. Builtin Signal Handlers

Right now (AFAIK) all the signal handlers _installed by django itself_ are synchronous. E.g. the signal that handles closing connections at the end of a request:

> <https://github.com/django/django/blob/adae619426b6f50046b3daaa744db52989c9d6db/django/db/ __init__.py#L54-L62>

We would need a way to represent that a signal can be called from either context (or to provide alternatives that don’t require sync/async boundary crossing). I’m not aware of any efforts in this area right now, but it would be a natural extension of the work in [Fixed #32172 -- Adapted signals to allow async handlers. by bigfootjon · Pull Request #16547 · django/django · GitHub](https://github.com/django/django/pull/16547).

## 2. Removing MiddlewareMixin

Most of Django’s built-in Middleware derives from `MiddlewareMixin` which unfortunately uses sync\_to\_async machinery to provide an async middleware interface:

> <https://github.com/django/django/blob/adae619426b6f50046b3daaa744db52989c9d6db/django/utils/deprecation.py#L135-L152>

There was some noise about this recently as a new class that extends `MiddlewareMixin` was introduced, but @adamchainz [pointed out some of the complexity with providing a native async solution](https://github.com/django/django/pull/17792#discussion_r1581437142) so there wasn’t any real progress towards resolution here.

## 3. Async ORM

Even if all of the problems above are resolved there’s still the underlying problem that the ORM is not async. In other words, let’s say we asyncify `AuthMiddleware` ([since that’s being actively worked on](https://github.com/django/django/pull/18036)). This would then use async methods to read from the database but these async methods still hit synchronous code paths inside the ORM. ☹ So this _moves_ the async boundary down one “level” in the call stack but it doesn’t eliminate it.

Full elimination would require async backends and an async ORM frontend which uses them, which are not actively being worked on to my knowledge (we would love it if it happened though).

If you’re interested in helping out with any of the above I think the signals problem is the most tractable right now, but no tickets have been filed so the first step would be getting a ticket accepted. I don’t really have the bandwidth to pick up more work on this. I’m tackling some [related work](https://github.com/django/channels/pull/2090#issuecomment-2132363909) in channels right now, but the above areas might be what I look at next.

CC @carltongibson for your thoughts as well

---

<div class="post-metadata">

**Author:** ![rob4226](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/rob4226/32/21682_2.png) [@rob4226](https://forum.djangoproject.com/u/rob4226)\
**Post date:** [June 6, 2024, 8:03am UTC](https://forum.djangoproject.com/t/sync-to-async-called-14-times-with-default-django-middleware-and-2-times-with-no-middleware/31701/3 "2024-06-06T08:03:18Z")

</div>

Thank you very much for that great information! It def helped me understand better how the sync/async gap is handled.

That is a really nice summary of what needs to be done to lessen Django’s calls to `sync_to_async`. I will take a closer look at the source code you linked to. Thank you!

---

<div class="post-metadata">

**Author:** ![jacobtylerwalls](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/jacobtylerwalls/32/30603_2.png) [@jacobtylerwalls](https://forum.djangoproject.com/u/jacobtylerwalls)\
**Post date:** [June 19, 2026, 10:42pm UTC](https://forum.djangoproject.com/t/sync-to-async-called-14-times-with-default-django-middleware-and-2-times-with-no-middleware/31701/4 "2026-06-19T22:42:54Z")

</div>

> [@bigfootjon](#):
>
> Most of Django’s built-in Middleware derives from `MiddlewareMixin` which unfortunately uses sync\_to\_async machinery to provide an async middleware interface:

See a PR to remove the repetitive context switching: [Fixed #37177 -- Reduced per-middleware context switching under ASGI. by jacobtylerwalls · Pull Request #21523 · django/django · GitHub](https://github.com/django/django/pull/21523). Reviews would be lovely! As well as comments on backward compatibility implications.
