[Django] #36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same request do not share the same event loop.

10 views
Skip to first unread message

Django

unread,
Jan 14, 2026, 8:20:12 AM (10 days ago) Jan 14
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
-------------------------------------+-------------------------------------
Reporter: Mykhailo Havelia | Type: Bug
Status: new | Component: HTTP
| handling
Version: 6.0 | Severity: Normal
Keywords: async, wsgi | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Each call creates a new thread with its own event loop. This commonly
happens in middlewares and signals, causing a single request to spawn
multiple threads and event loops, which is unnecessary and prevents reuse
of async resources.

This could be addressed by introducing a per-request async context, for
example as shown in this draft implementation:
https://github.com/Arfey/django/pull/5/changes
--
Ticket URL: <https://code.djangoproject.com/ticket/36863>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Jan 14, 2026, 10:35:13 AM (10 days ago) Jan 14
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: (none)
Type: Bug | Status: new
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Changes (by Flavio Curella):

* cc: Flavio Curella (added)

--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:1>

Django

unread,
Jan 15, 2026, 4:48:56 AM (9 days ago) Jan 15
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: (none)
Type: Bug | Status: new
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Comment (by Kundan Yadav):

hey can i work on this issue ?
--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:2>

Django

unread,
Jan 15, 2026, 9:10:46 AM (9 days ago) Jan 15
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: (none)
Type: Bug | Status: new
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Comment (by Jacob Walls):

Thanks, but you should wait for it to be accepted first.
--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:3>

Django

unread,
Jan 17, 2026, 12:13:02 AM (8 days ago) Jan 17
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: (none)
Type: Bug | Status: new
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Comment (by Vishy Algo):

With asgiref >= 3.10, we can leverage AsyncSingleThreadContext to enforce
a persistent execution context for the entire request lifecycle.

I suggest wrapping the {{{ WSGIHandler.__call__ }}} logic in this context.
Crucially, because the WSGI response iteration often outlives the {{{
__call__ }}} stack frame (e.g. StreamingHttpResponse), we cannot use a
simple with block or decorator. Instead, we must manually manage the
context's lifecycle and attach the cleanup logic to response.close().

I’ve verified this behavior with a test asserting that the thread is
successfully reused across calls.

{{{#!diff
diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py
index aab9fe0c49..d531c6a564 100644
--- a/django/core/handlers/wsgi.py
+++ b/django/core/handlers/wsgi.py
@@ -118,30 +118,45 @@ class WSGIHandler(base.BaseHandler):
self.load_middleware()

def __call__(self, environ, start_response):
- set_script_prefix(get_script_name(environ))
- signals.request_started.send(sender=self.__class__,
environ=environ)
- request = self.request_class(environ)
- response = self.get_response(request)
-
- response._handler_class = self.__class__
-
- status = "%d %s" % (response.status_code, response.reason_phrase)
- response_headers = [
- *response.items(),
- *(("Set-Cookie", c.OutputString()) for c in
response.cookies.values()),
- ]
- start_response(status, response_headers)
- if getattr(response, "file_to_stream", None) is not None and
environ.get(
- "wsgi.file_wrapper"
- ):
- # If `wsgi.file_wrapper` is used the WSGI server does not
call
- # .close on the response, but on the file wrapper. Patch it
to use
- # response.close instead which takes care of closing all
files.
- response.file_to_stream.close = response.close
- response = environ["wsgi.file_wrapper"](
- response.file_to_stream, response.block_size
- )
- return response
+ async_context = AsyncSingleThreadContext()
+ async_context.__enter__()
+ try:
+ set_script_prefix(get_script_name(environ))
+ signals.request_started.send(sender=self.__class__,
environ=environ)
+ request = self.request_class(environ)
+ response = self.get_response(request)
+
+ response._handler_class = self.__class__
+
+ status = "%d %s" % (response.status_code,
response.reason_phrase)
+ response_headers = [
+ *response.items(),
+ *(("Set-Cookie", c.OutputString()) for c in
response.cookies.values()),
+ ]
+ start_response(status, response_headers)
+
+ original_close = response.close
+
+ def close():
+ try:
+ original_close()
+ finally:
+ async_context.__exit__(None, None, None)
+
+ if getattr(response, "file_to_stream", None) is not None and
environ.get(
+ "wsgi.file_wrapper"
+ ):
+ # If `wsgi.file_wrapper` is used the WSGI server does not
call
+ # .close on the response, but on the file wrapper. Patch
it to use
+ # response.close instead which takes care of closing all
files.
+ response.file_to_stream.close = response.close
+ response = environ["wsgi.file_wrapper"](
+ response.file_to_stream, response.block_size
+ )
+ return response
+ except Exception:
+ async_context.__exit__(None, None, None)
+ raise
}}}


{{{#!python
def test_async_context_reuse(self):
"""
Multiple calls to async_to_sync within a single request share the
same thread and event loop via AsyncSingleThreadContext.
"""
async def get_thread_ident():
return threading.get_ident()

class ProbingHandler(WSGIHandler):
def __init__(self):
pass

def get_response(self, request):
t1 = async_to_sync(get_thread_ident)()
t2 = async_to_sync(get_thread_ident)()
return HttpResponse(f"{t1}|{t2}")

app = ProbingHandler()
environ = self.request_factory._base_environ(PATH_INFO="/")

def start_response(status, headers):
pass

response = app(environ, start_response)
content = b"".join(response).decode("utf-8")
t1, t2 = content.split("|")

self.assertEqual(
t1, t2,
f"Failed: async_to_sync spawned new threads ({t1} vs {t2}).
Context was not reused."
)
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:4>

Django

unread,
Jan 17, 2026, 3:03:28 AM (8 days ago) Jan 17
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: Vishy Algo
Type: Bug | Status: assigned
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Changes (by Vishy Algo):

* owner: (none) => Vishy Algo
* status: new => assigned

--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:5>

Django

unread,
Jan 17, 2026, 7:09:03 PM (7 days ago) Jan 17
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: Vishy Algo
Type: Bug | Status: assigned
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Comment (by Mykhailo Havelia):

Replying to [comment:4 Vishy Algo]:

Okay. I’m going to add tests for my MR and then submit it for code review.
--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:6>

Django

unread,
Jan 17, 2026, 8:54:45 PM (7 days ago) Jan 17
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: Vishy Algo
Type: Bug | Status: assigned
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Comment (by Mykhailo Havelia):

Replying to [comment:4 Vishy Algo]:

I prepared an MR, but it seems it's not sufficient:
https://github.com/django/django/pull/20552. I added a test that compares
not just the thread, but the event loop as well, and the event loop isn't
the same. asgiref uses the same thread, but it starts a new event loop for
each function via asyncio.run
(https://github.com/django/asgiref/blob/main/asgiref/sync.py#L314).
I could try creating a loop myself and run each function via
loop.run_until_complete, but that would require some additional
investigation to see if it's feasible.

What do you think?
--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:7>

Django

unread,
Jan 21, 2026, 11:21:18 AM (3 days ago) Jan 21
to django-...@googlegroups.com
#36863: Under WSGI, multiple calls to asgiref.sync.async_to_sync within the same
request do not share the same event loop.
----------------------------------+--------------------------------------
Reporter: Mykhailo Havelia | Owner: Vishy Algo
Type: Bug | Status: assigned
Component: HTTP handling | Version: 6.0
Severity: Normal | Resolution:
Keywords: async, wsgi | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Comment (by Mykhailo Havelia):

Replying to [comment:3 Jacob Walls]:

I've added an MR with changes to asgiref that should help with sharing the
same event loop. There are still some issues on lower Python versions, but
the overall approach can already be reviewed 😌
https://github.com/django/asgiref/pull/542
--
Ticket URL: <https://code.djangoproject.com/ticket/36863#comment:8>
Reply all
Reply to author
Forward
0 new messages