Asynchronous topics by Django documentation ¶
See also
Django 3.1 ¶
See also
Asynchronous views and middleware support ¶
Django now supports a fully asynchronous request path, including:
To get started with async views, you need to declare a view using async def :
1 async def my_view(request):
2 await asyncio.sleep(0.5)
3 return HttpResponse('Hello, async world!')
All asynchronous features are supported whether you are running under WSGI or ASGI mode.
However, there will be performance penalties using async code in WSGI mode.
You can read more about the specifics in Asynchronous support documentation.
You are free to mix async and sync views, middleware, and tests as much as you want. Django will ensure that you always end up with the right execution context.
We expect most projects will keep the majority of their views synchronous, and only have a select few running in async mode - but it is entirely your choice.
Django’s ORM, cache layer, and other pieces of code that do long-running network calls do not yet support async access. We expect to add support for them in upcoming releases.
Async views are ideal, however, if you are doing a lot of API or HTTP calls inside your view, you can now natively do all those HTTP calls in parallel to considerably speed up your view’s execution.
Asynchronous support should be entirely backwards-compatible and we have tried to ensure that it has no speed regressions for your existing, synchronous code. It should have no noticeable effect on any existing Django projects.
topics/http/views Async views ¶
As well as being synchronous functions, views can also be asynchronous (“async”) functions, normally defined using Python’s async def syntax.
Django will automatically detect these and run them in an async context.
However, you will need to use an async server based on ASGI to get their performance benefits .
Here’s an example of an async view:
1 import datetime
2
3 # https://docs.python.org/3.9/library/zoneinfo.html
4 from zoneinfo import ZoneInfo
5
6 from django.http import HttpResponse
7
8 async def current_datetime(request):
9 now = datetime.datetime.now(tz=ZoneInfo("Europe/Paris"))
10 html = '<html><body>It is now %s.</body></html>' % now
11 return HttpResponse(html)
You can read more about Django’s async support, and how to best use async views, in Asynchronous support.
topics/http/middleware async-middleware Asynchronous support ¶
Middleware can support any combination of synchronous and asynchronous requests.
Django will adapt requests to fit the middleware’s requirements if it cannot support both, but at a performance penalty.
By default, Django assumes that your middleware is capable of handling only synchronous requests.
To change these assumptions, set the following attributes on your middleware factory function or class:
-
sync_capable is a boolean indicating if the middleware can handle synchronous requests. Defaults to True .
-
async_capable is a boolean indicating if the middleware can handle asynchronous requests. Defaults to False .
If your middleware has both sync_capable = True and async_capable = True, then Django will pass it the request without converting it.
In this case, you can work out if your middleware will receive async requests by checking if the get_response object you are passed is a coroutine function, using asyncio.iscoroutinefunction() .
The django.utils.decorators module contains:
-
sync_only_middleware() ,
-
async_only_middleware(),
-
and sync_and_async_middleware()
decorators that allow you to apply these flags to middleware factory functions.
The returned callable must match the sync or async nature of the get_response method. If you have an asynchronous get_response, you must return a coroutine function (async def).
process_view, process_template_response and process_exception methods, if they are provided, should also be adapted to match the sync/async mode.
However, Django will individually adapt them as required if you do not, at an additional performance penalty.
Here’s an example of how to create a middleware function that supports both:
1 import asyncio
2 from django.utils.decorators import sync_and_async_middleware
3
4 @sync_and_async_middleware
5 def simple_middleware(get_response):
6 # One-time configuration and initialization goes here.
7 if asyncio.iscoroutinefunction(get_response):
8 async def middleware(request):
9 # Do something here!
10 response = await get_response(request)
11 return response
12
13 else:
14 def middleware(request):
15 # Do something here!
16 response = get_response(request)
17 return response
18
19 return middleware
Note
If you declare a hybrid middleware that supports both synchronous and asynchronous calls, the kind of call you get may not match the underlying view. Django will optimize the middleware call stack to have as few sync/async transitions as possible.
Thus, even if you are wrapping an async view, you may be called in sync mode if there is other, synchronous middleware between you and the view.
topics/testing/tools/#async-tests Testing asynchronous code ¶
If you merely want to test the output of your asynchronous views, the standard test client will run them inside their own asynchronous loop without any extra work needed on your part.
However, if you want to write fully-asynchronous tests for a Django project, you will need to take several things into account.
Firstly, your tests must be async def methods on the test class (in order to give them an asynchronous context).
Django will automatically detect any async def tests and wrap them so they run in their own event loop.
If you are testing from an asynchronous function, you must also use the asynchronous test client. This is available as django.test.AsyncClient, or as self.async_client on any test.
With the exception of the follow parameter, which is not supported, AsyncClient has the same methods and signatures as the synchronous (normal) test client, but any method that makes a request must be awaited:
1 async def test_my_thing(self):
2 response = await self.async_client.get('/some-url/')
3 self.assertEqual(response.status_code, 200)
The asynchronous client can also call synchronous views; it runs through Django’s asynchronous request path , which supports both.
Any view called through the AsyncClient will get an ASGIRequest object for its request rather than the WSGIRequest that the normal client creates.
topics/async/ Introduction ¶
Django has support for writing asynchronous (“async”) views, along with an entirely async-enabled request stack if you are running under ASGI.
Async views will still work under WSGI, but with performance penalties , and without the ability to have efficient long-running requests.
We’re still working on async support for the ORM and other parts of Django. You can expect to see this in future releases.
For now, you can use the sync_to_async() adapter to interact with the sync parts of Django.
There is also a whole range of async-native Python libraries that you can integrate with.
Async views ¶
Any view can be declared async by making the callable part of it return a coroutine, commonly, this is done using async def .
For a function-based view, this means declaring the whole view using async def.
For a class-based view, this means making its __call__() method an async def (not its __init__() or as_view()).
Note
Django uses asyncio.iscoroutinefunction to test if your view is asynchronous or not. If you implement your own method of returning a coroutine, ensure you set the _is_coroutine attribute of the view to asyncio.coroutines._is_coroutine so this function returns True.
Under a WSGI server, async views will run in their own, one-off event loop. This means you can use async features, like concurrent async HTTP requests, without any issues, but you will not get the benefits of an async stack .
The main benefits are the ability to service hundreds of connections without using Python threads .
This allows you to use slow streaming, long-polling , and other exciting response types.
If you want to use these, you will need to deploy Django using ASGI instead.
In both ASGI and WSGI mode, you can still safely use asynchronous support to run code concurrently rather than serially. This is especially handy when dealing with external APIs or data stores.
If you want to call a part of Django that is still synchronous, like the ORM, you will need to wrap it in a sync_to_async() call.
For example (to be completed, see Example 3 async Django )
1 # https://docs.python.org/3/library/asyncio-task.html
2 import asyncio
3 # https://docs.djangoproject.com/en/3.1/topics/async/#async-to-sync
4 from asgiref.sync import sync_to_async
5
6
7 from asgiref.sync import sync_to_async
8 results = sync_to_async(Blog.objects.get)(pk=123)
9
10 loop = asyncio.get_event_loop()
11 loop.create_task(get_blog)(pk)
You may find it easier to move any ORM code into its own function and call that entire function using sync_to_async() .
For example (to be completed, see Example 3 async Django )
1 # https://docs.python.org/3/library/asyncio-task.html
2 import asyncio
3 # https://docs.djangoproject.com/en/3.1/topics/async/#async-to-sync
4 from asgiref.sync import sync_to_async
5
6 @sync_to_async
7 def get_blog(pk):
8 return Blog.objects.select_related('author').get(pk=pk)
9
10
11 loop = asyncio.get_event_loop()
12 loop.create_task(get_blog)(pk)
If you accidentally try to call a part of Django that is still synchronous-only from an async view, you will trigger Django’s asynchronous safety protection to protect your data from corruption.
Performance ¶
When running in a mode that does not match the view (e.g. an async view under WSGI, or a traditional sync view under ASGI), Django must emulate the other call style to allow your code to run.
This context-switch causes a small performance penalty of around a millisecond.
This is also true of middleware. Django will attempt to minimize the number of context-switches between sync and async.
If you have an ASGI server, but all your middleware and views are synchronous, it will switch just once, before it enters the middleware stack.
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.
You should do your own performance testing to see what effect ASGI versus WSGI has on your code.
In some cases, there may be a performance increase even for a purely synchronous codebase under ASGI because the request-handling code is still all running asynchronously.
In general you will only want to enable ASGI mode if you have asynchronous code in your project.
django.core.handlers.asgi.py ¶
1import logging
2import sys
3import tempfile
4import traceback
5
6from asgiref.sync import sync_to_async
7
8from django.conf import settings
9from django.core import signals
10from django.core.exceptions import RequestAborted, RequestDataTooBig
11from django.core.handlers import base
12from django.http import (
13 FileResponse,
14 HttpRequest,
15 HttpResponse,
16 HttpResponseBadRequest,
17 HttpResponseServerError,
18 QueryDict,
19 parse_cookie,
20)
21from django.urls import set_script_prefix
22from django.utils.functional import cached_property
23
24logger = logging.getLogger("django.request")
25
26
27class ASGIRequest(HttpRequest):
28 """
29 Custom request subclass that decodes from an ASGI-standard request dict
30 and wraps request body handling.
31 """
32
33 # Number of seconds until a Request gives up on trying to read a request
34 # body and aborts.
35 body_receive_timeout = 60
36
37 def __init__(self, scope, body_file):
38 self.scope = scope
39 self._post_parse_error = False
40 self._read_started = False
41 self.resolver_match = None
42 self.script_name = self.scope.get("root_path", "")
43 if self.script_name and scope["path"].startswith(self.script_name):
44 # TODO: Better is-prefix checking, slash handling?
45 self.path_info = scope["path"][len(self.script_name) :]
46 else:
47 self.path_info = scope["path"]
48 # The Django path is different from ASGI scope path args, it should
49 # combine with script name.
50 if self.script_name:
51 self.path = "%s/%s" % (
52 self.script_name.rstrip("/"),
53 self.path_info.replace("/", "", 1),
54 )
55 else:
56 self.path = scope["path"]
57 # HTTP basics.
58 self.method = self.scope["method"].upper()
59 # Ensure query string is encoded correctly.
60 query_string = self.scope.get("query_string", "")
61 if isinstance(query_string, bytes):
62 query_string = query_string.decode()
63 self.META = {
64 "REQUEST_METHOD": self.method,
65 "QUERY_STRING": query_string,
66 "SCRIPT_NAME": self.script_name,
67 "PATH_INFO": self.path_info,
68 # WSGI-expecting code will need these for a while
69 "wsgi.multithread": True,
70 "wsgi.multiprocess": True,
71 }
72 if self.scope.get("client"):
73 self.META["REMOTE_ADDR"] = self.scope["client"][0]
74 self.META["REMOTE_HOST"] = self.META["REMOTE_ADDR"]
75 self.META["REMOTE_PORT"] = self.scope["client"][1]
76 if self.scope.get("server"):
77 self.META["SERVER_NAME"] = self.scope["server"][0]
78 self.META["SERVER_PORT"] = str(self.scope["server"][1])
79 else:
80 self.META["SERVER_NAME"] = "unknown"
81 self.META["SERVER_PORT"] = "0"
82 # Headers go into META.
83 for name, value in self.scope.get("headers", []):
84 name = name.decode("latin1")
85 if name == "content-length":
86 corrected_name = "CONTENT_LENGTH"
87 elif name == "content-type":
88 corrected_name = "CONTENT_TYPE"
89 else:
90 corrected_name = "HTTP_%s" % name.upper().replace("-", "_")
91 # HTTP/2 say only ASCII chars are allowed in headers, but decode
92 # latin1 just in case.
93 value = value.decode("latin1")
94 if corrected_name in self.META:
95 value = self.META[corrected_name] + "," + value
96 self.META[corrected_name] = value
97 # Pull out request encoding, if provided.
98 self._set_content_type_params(self.META)
99 # Directly assign the body file to be our stream.
100 self._stream = body_file
101 # Other bits.
102 self.resolver_match = None
103
104 @cached_property
105 def GET(self):
106 return QueryDict(self.META["QUERY_STRING"])
107
108 def _get_scheme(self):
109 return self.scope.get("scheme") or super()._get_scheme()
110
111 def _get_post(self):
112 if not hasattr(self, "_post"):
113 self._load_post_and_files()
114 return self._post
115
116 def _set_post(self, post):
117 self._post = post
118
119 def _get_files(self):
120 if not hasattr(self, "_files"):
121 self._load_post_and_files()
122 return self._files
123
124 POST = property(_get_post, _set_post)
125 FILES = property(_get_files)
126
127 @cached_property
128 def COOKIES(self):
129 return parse_cookie(self.META.get("HTTP_COOKIE", ""))
130
131
132class ASGIHandler(base.BaseHandler):
133 """Handler for ASGI requests."""
134
135 request_class = ASGIRequest
136 # Size to chunk response bodies into for multiple response messages.
137 chunk_size = 2 ** 16
138
139 def __init__(self):
140 super().__init__()
141 self.load_middleware(is_async=True)
142
143 async def __call__(self, scope, receive, send):
144 """
145 Async entrypoint - parses the request and hands off to get_response.
146 """
147 # Serve only HTTP connections.
148 # FIXME: Allow to override this.
149 if scope["type"] != "http":
150 raise ValueError(
151 "Django can only handle ASGI/HTTP connections, not %s." % scope["type"]
152 )
153 # Receive the HTTP request body as a stream object.
154 try:
155 body_file = await self.read_body(receive)
156 except RequestAborted:
157 return
158 # Request is complete and can be served.
159 set_script_prefix(self.get_script_prefix(scope))
160 await sync_to_async(signals.request_started.send, thread_sensitive=True)(
161 sender=self.__class__, scope=scope
162 )
163 # Get the request and check for basic issues.
164 request, error_response = self.create_request(scope, body_file)
165 if request is None:
166 await self.send_response(error_response, send)
167 return
168 # Get the response, using the async mode of BaseHandler.
169 response = await self.get_response_async(request)
170 response._handler_class = self.__class__
171 # Increase chunk size on file responses (ASGI servers handles low-level
172 # chunking).
173 if isinstance(response, FileResponse):
174 response.block_size = self.chunk_size
175 # Send the response.
176 await self.send_response(response, send)
177
178 async def read_body(self, receive):
179 """Reads a HTTP body from an ASGI connection."""
180 # Use the tempfile that auto rolls-over to a disk file as it fills up.
181 body_file = tempfile.SpooledTemporaryFile(
182 max_size=settings.FILE_UPLOAD_MAX_MEMORY_SIZE, mode="w+b"
183 )
184 while True:
185 message = await receive()
186 if message["type"] == "http.disconnect":
187 # Early client disconnect.
188 raise RequestAborted()
189 # Add a body chunk from the message, if provided.
190 if "body" in message:
191 body_file.write(message["body"])
192 # Quit out if that's the end.
193 if not message.get("more_body", False):
194 break
195 body_file.seek(0)
196 return body_file
197
198 def create_request(self, scope, body_file):
199 """
200 Create the Request object and returns either (request, None) or
201 (None, response) if there is an error response.
202 """
203 try:
204 return self.request_class(scope, body_file), None
205 except UnicodeDecodeError:
206 logger.warning(
207 "Bad Request (UnicodeDecodeError)",
208 exc_info=sys.exc_info(),
209 extra={"status_code": 400},
210 )
211 return None, HttpResponseBadRequest()
212 except RequestDataTooBig:
213 return None, HttpResponse("413 Payload too large", status=413)
214
215 def handle_uncaught_exception(self, request, resolver, exc_info):
216 """Last-chance handler for exceptions."""
217 # There's no WSGI server to catch the exception further up
218 # if this fails, so translate it into a plain text response.
219 try:
220 return super().handle_uncaught_exception(request, resolver, exc_info)
221 except Exception:
222 return HttpResponseServerError(
223 traceback.format_exc() if settings.DEBUG else "Internal Server Error",
224 content_type="text/plain",
225 )
226
227 async def send_response(self, response, send):
228 """Encode and send a response out over ASGI."""
229 # Collect cookies into headers. Have to preserve header case as there
230 # are some non-RFC compliant clients that require e.g. Content-Type.
231 response_headers = []
232 for header, value in response.items():
233 if isinstance(header, str):
234 header = header.encode("ascii")
235 if isinstance(value, str):
236 value = value.encode("latin1")
237 response_headers.append((bytes(header), bytes(value)))
238 for c in response.cookies.values():
239 response_headers.append(
240 (b"Set-Cookie", c.output(header="").encode("ascii").strip())
241 )
242 # Initial response message.
243 await send(
244 {
245 "type": "http.response.start",
246 "status": response.status_code,
247 "headers": response_headers,
248 }
249 )
250 # Streaming responses need to be pinned to their iterator.
251 if response.streaming:
252 # Access `__iter__` and not `streaming_content` directly in case
253 # it has been overridden in a subclass.
254 for part in response:
255 for chunk, _ in self.chunk_bytes(part):
256 await send(
257 {
258 "type": "http.response.body",
259 "body": chunk,
260 # Ignore "more" as there may be more parts; instead,
261 # use an empty final closing message with False.
262 "more_body": True,
263 }
264 )
265 # Final closing message.
266 await send({"type": "http.response.body"})
267 # Other responses just need chunking.
268 else:
269 # Yield chunks of response.
270 for chunk, last in self.chunk_bytes(response.content):
271 await send(
272 {
273 "type": "http.response.body",
274 "body": chunk,
275 "more_body": not last,
276 }
277 )
278 await sync_to_async(response.close, thread_sensitive=True)()
279
280 @classmethod
281 def chunk_bytes(cls, data):
282 """
283 Chunks some data up so it can be sent in reasonable size messages.
284 Yields (chunk, last_chunk) tuples.
285 """
286 position = 0
287 if not data:
288 yield data, True
289 return
290 while position < len(data):
291 yield (
292 data[position : position + cls.chunk_size],
293 (position + cls.chunk_size) >= len(data),
294 )
295 position += cls.chunk_size
296
297 def get_script_prefix(self, scope):
298 """
299 Return the script prefix to use from either the scope or a setting.
300 """
301 if settings.FORCE_SCRIPT_NAME:
302 return settings.FORCE_SCRIPT_NAME
303 return scope.get("root_path", "") or ""